More refactoring

This commit is contained in:
GDay
2023-03-31 02:09:54 +02:00
parent a694a9f53c
commit e3517bd4c6
16 changed files with 715 additions and 750 deletions

View File

@@ -183,7 +183,7 @@ class Sentinel:
self.scheduler = Scheduler()
# set worker cpu affinity if needed
if psutil and Conf.CPU_AFFINITY:
set_cpu_affinity(Conf.CPU_AFFINITY, [w.process.process_id for w in self.pool.workers])
set_cpu_affinity(Conf.CPU_AFFINITY, [w.process.pid for w in self.pool.workers])
def guard(self):

View File

@@ -1,4 +1,5 @@
import signal
from typing import Optional
class TimeoutException(SystemExit):
"""Exception for when a worker takes too long to complete a task"""
@@ -6,7 +7,7 @@ class TimeoutException(SystemExit):
class TimeoutHandler:
def __init__(self, timeout: int):
def __init__(self, timeout: Optional[int] = None):
self._timeout = timeout
def raise_timeout_exception(self, signum, frame):
@@ -14,6 +15,8 @@ class TimeoutHandler:
'({0} seconds)'.format(self._timeout))
def __enter__(self):
if self._timeout is None:
return
signal.signal(signal.SIGALRM, self.raise_timeout_exception)
signal.alarm(self._timeout)

43
django_q/helpers.py Normal file
View File

@@ -0,0 +1,43 @@
from copy import Error
from django_q.worker import WorkerProcess
from django_q.monitor import Monitor
from django_q.models import Task
from typing import Optional, Sequence, Tuple
from django_q.conf import logger
from django_q.queue_task import QueueTask
from django_q.puller import Puller
from django_q.scheduler import Scheduler
def run_scheduler_once(broker=None) -> None:
Scheduler.schedule_tasks(broker)
def get_scheduled_tasks(broker=None) -> Sequence[QueueTask]:
try:
return Puller.get_tasks_from_broker(broker=broker)
except ValueError:
logger.exception("Couldn't get items from broker")
return []
def run_task(task=None) -> QueueTask:
if task is None:
scheduled_tasks = get_scheduled_tasks()
if not len(scheduled_tasks):
raise ValueError("No tasks scheduled and no task given to run for worker")
task = scheduled_tasks[0]
return WorkerProcess.run_task(task)
def save_task(task, broker=None) -> Tuple[QueueTask, Optional[Task]]:
return Monitor.save_task(task, broker)
def run_cluster_once(workers, tasks=[], broker=None) -> None:
if not len(tasks):
run_scheduler_once(broker=broker)
tasks = get_scheduled_tasks(broker=broker)
for idx, worker in enumerate(range(workers)):
if len(tasks) >= idx + 1:
task = run_task(tasks[idx])
save_task(task, broker=broker)

View File

@@ -1,5 +1,6 @@
from datetime import datetime, timedelta
from keyword import iskeyword
import ast
# Django
from django import get_version
@@ -222,6 +223,34 @@ class Schedule(models.Model):
help_text=_("Name of kwarg to pass intended schedule date"),
)
def parse_kwargs(self):
if not self.kwargs:
return {}
try:
# first try the dict syntax
return ast.literal_eval(self.kwargs)
except (SyntaxError, ValueError):
# else use the kwargs syntax
try:
parsed_kwargs = (
ast.parse(f"f({self.kwargs})").body[0].value.keywords
)
return {
kwarg.arg: ast.literal_eval(kwarg.value)
for kwarg in parsed_kwargs
}
except (SyntaxError, ValueError):
return {}
def parse_args(self):
if not self.args:
return tuple()
args = ast.literal_eval(self.args)
# single value won't eval to tuple, so:
if type(args) != tuple:
args = (args,)
return args
def calculate_next_run(self, next_run=None):
# next run is always in UTC
next_run = next_run or self.next_run

View File

@@ -1,5 +1,9 @@
from django_q.worker import WorkerProcess
from django_q.queue_task import QueueTask
from django_q.models import Task
from queue import Queue
from queue import Empty
from typing import Optional, Tuple
from django_q.brokers import get_broker
from django_q.process_manager import ProcessManager
from django_q.signals import post_execute
@@ -18,6 +22,24 @@ class Monitor(ProcessManager):
super().__init__()
self.task_queue = Queue()
@staticmethod
def save_task(task, broker=None) -> Tuple[QueueTask, Optional[Task]]:
task_db_obj = None
if broker is None:
broker = get_broker()
if task.cached:
task.save_cached(broker)
else:
print("SAVE TO DB")
task_db_obj = task.save_to_db(broker)
# acknowledge result
if task.ack_id and (task.has_succeeded or not task.ack_failure):
broker.acknowledge(task.ack_id)
# signal execution done
post_execute.send(sender="django_q", task=task)
return task, task_db_obj
@property
def is_done(self):
return self.status.value == self.Status.IDLE.value and self.task_queue.empty()
@@ -32,7 +54,11 @@ class Monitor(ProcessManager):
except Empty:
# if the queue is empty, then just stop
return
self.manager_pipe.send(task)
try:
self.manager_pipe.send(task)
except BrokenPipeError:
# recycle process if pipe is broken
self.status.value = ProcessManager.Status.RECYCLE.value
def add_task(self, task):
self.task_queue.put(task)
@@ -43,8 +69,8 @@ class Monitor(ProcessManager):
if setproctitle:
setproctitle.setproctitle(f"qcluster {proc_name} monitor")
logger.info(
_("%(name)s monitoring at %(id)s") % {"name": proc_name, "id": current_process().pid}
)
_("%(name)s monitoring at %(id)s") % {"name": proc_name, "id": current_process().pid}
)
status.value = self.Status.IDLE.value
while True:
@@ -54,32 +80,23 @@ class Monitor(ProcessManager):
break
status.value = self.Status.BUSY.value
# save the result
if task.cached:
task.save_cached(broker)
else:
task.save_to_db(broker)
# acknowledge result
if task.ack_id and (not task.has_succeeded or task.ack_failure):
broker.acknowledge(task.ack_id)
# signal execution done
post_execute.send(sender="django_q", task=task)
task, __ = Monitor.save_task(task, broker=broker)
# log the result
if task.has_succeeded:
# log success
logger.info(
_("Processed '%(info_name)s' (%(task_name)s)")
% {"info_name": task.func_name, "task_name": task.name}
)
_("Processed '%(info_name)s' (%(task_name)s)")
% {"info_name": task.func_name, "task_name": task.name}
)
else:
# log failure
logger.error(
_("Failed '%(info_name)s' (%(task_name)s) - %(task_result)s")
% {
"info_name": task.func_name,
"task_name": task.name,
"task_result": task.result_payload,
}
)
_("Failed '%(info_name)s' (%(task_name)s) - %(task_result)s")
% {
"info_name": task.func_name,
"task_name": task.name,
"task_result": task.result,
}
)
status.value = self.Status.IDLE.value
logger.info(_("%(name)s stopped monitoring results") % {"name": proc_name})

View File

@@ -16,10 +16,45 @@ from django_q.exceptions import TimeoutException, TimeoutHandler
from django_q.process_manager import ProcessManager
class Puller(ProcessManager):
"""The Puller is responsible for pulling the tasks from the broker, then return them to be picked up by the
guard"""
@staticmethod
def get_tasks_from_broker(broker=None):
queued_tasks = []
logger.debug("pulling new tasks")
if broker is None:
broker = get_broker()
try:
task_set = broker.dequeue()
except Exception:
# broker probably crashed. Let the sentinel handle it.
raise ValueError("Failed to pull task from broker")
if task_set:
logger.info(
_("Found %(amount_tasks)s tasks") % {"amount_tasks": len(task_set)}
)
for task in task_set:
print(task)
logger.info("ONE TASK")
ack_id = task[0]
# unpack the task
try:
queue_task = SignedPackage.loads(task[1])
except (TypeError, BadSignature):
logger.exception("Failed to pull task from broker - bad task")
broker.fail(ack_id)
continue
queue_task.ack_id = ack_id
# send back to main process
queued_tasks.append(queue_task)
logger.debug(
_("queueing from %(list_key)s") % {"list_key": broker.list_key}
)
return queued_tasks
def get_target(self):
return self.run_puller
@@ -40,29 +75,11 @@ class Puller(ProcessManager):
logger.info("Stopping Puller")
break
try:
task_set = broker.dequeue()
queued_tasks = Puller.get_tasks_from_broker(broker=broker)
except Exception:
logger.exception("Failed to pull task from broker")
# broker probably crashed. Let the sentinel handle it.
logger.exception("Couldn't get items from broker")
sleep(10)
break
if task_set:
logger.info(
_("Found %(amount_tasks)s tasks") % {"amount_tasks": len(task_set)}
)
for task in task_set:
ack_id = task[0]
# unpack the task
try:
queue_task = SignedPackage.loads(task[1])
except (TypeError, BadSignature):
logger.exception("Failed to pull task from broker - bad task")
broker.fail(ack_id)
continue
queue_task.ack_id = ack_id
# send back to main process
pipe.send(queue_task)
logger.debug(
_("queueing from %(list_key)s") % {"list_key": broker.list_key}
)
for queue_task in queued_tasks:
pipe.send(queue_task)
logger.info(_("%(name)s stopped pushing tasks") % {"name": current_process().name})

View File

@@ -25,25 +25,25 @@ class QueueTask:
func: Union[Callable, str]
name: str
group: Optional[None] = None
group: Optional[str] = None
queued_at: Optional[datetime] = timezone.now()
finished_at: Optional[datetime] = None
ack_id: Optional[str] = None
started_at: Optional[datetime] = None
id: str = "-1"
timeout: Union[int, None] = Conf.TIMEOUT
result: Union[Result, None] = None
result_payload: Any = None
timeout: Optional[int] = Conf.TIMEOUT
result_status: Optional[Result] = None
result: Any = None
save: bool = Conf.SAVE_LIMIT >= 0
chain: Union[str, QueueTask] = ""
cached: bool = False
sync: bool = False
hook: Union[str, None] = None
cached: bool = Conf.CACHED
sync: bool = Conf.SYNC
hook: Optional[str] = None
args: tuple = field(default_factory=tuple)
kwargs: dict = field(default_factory=dict)
ack_failure: bool = Conf.ACK_FAILURES
iter_count: Union[int, None] = None
iter_cached: Union[int, None] = None
iter_count: Optional[int] = None
iter_cached: Optional[int] = None
def callable_func(self):
func = self.func
@@ -53,11 +53,11 @@ class QueueTask:
@property
def has_succeeded(self):
return self.result == self.Result.SUCCESS
return self.result_status == self.Result.SUCCESS
@property
def has_timed_out(self):
return self.result == self.Result.TIMEOUT
return self.result_status == self.Result.TIMEOUT
@property
def is_callable(self):
@@ -126,7 +126,7 @@ class QueueTask:
'args': self.args,
'kwargs': self.kwargs,
'started': self.started_at,
'result': self.result_payload,
'result': self.result,
'group': self.group,
'success': self.has_succeeded,
'attempt_count': 1
@@ -136,8 +136,9 @@ class QueueTask:
# only update the result if it hasn't succeeded yet
if not created and not existing_task.success:
existing_task.stopped = self.finished_at
existing_task.result = self.result_payload
existing_task.result = self.result
existing_task.success = self.has_succeeded
existing_task.attempt_count += 1
existing_task.save()
if (
@@ -146,6 +147,8 @@ class QueueTask:
):
broker.acknowledge(self.ack_id)
return existing_task
except Exception:
logger.exception("Could not save task result")
@@ -167,11 +170,11 @@ class QueueTask:
group_args = f"{broker.list_key}:{group}:args"
# collate the results into a Task result
results = [
SignedPackage.get_queue_task(broker.cache.get(k)).result_payload
SignedPackage.loads(broker.cache.get(k)).result
for k in group_list
]
results.append(self.result_payload)
self.result_payload = results
results.append(self.result)
self.result = results
self.id = group
self.args = SignedPackage.loads(broker.cache.get(group_args))
self.iter_count = None

View File

@@ -1,4 +1,5 @@
from django_q.utils import localtime
import uuid
from django_q.models import Schedule
from django_q import tasks
import ast
@@ -8,22 +9,100 @@ from time import sleep
from django_q.brokers import get_broker
from django.utils import timezone
from multiprocessing import current_process
from multiprocessing import Value, current_process
from django_q.utils import close_old_django_connections
from django.utils.translation import gettext_lazy as _
from django_q.conf import Conf, logger
from django_q.process_manager import ProcessManager
class Scheduler(ProcessManager):
"""The Scheduler is responsible for scheduling new tasks"""
@staticmethod
def schedule_tasks(broker=None):
logger.debug("Start sheduling")
if broker is None:
broker = get_broker()
with db.transaction.atomic(using=db.router.db_for_write(Schedule)):
for s in (
Schedule.objects.select_for_update()
.exclude(repeats=0)
.filter(db.models.Q(next_run__lt=timezone.now()), db.models.Q(cluster__isnull=True) | db.models.Q(cluster=Conf.PREFIX))
):
args = s.parse_args()
kwargs = s.parse_kwargs()
q_options = kwargs.get("q_options", {})
if s.intended_date_kwarg:
kwargs[s.intended_date_kwarg] = s.next_run.isoformat()
if s.hook:
q_options["hook"] = s.hook
# set up the next run time
if s.schedule_type != s.ONCE:
next_run = s.calculate_next_run(s.next_run)
if not Conf.CATCH_UP:
while next_run <= localtime():
next_run = s.calculate_next_run(next_run)
s.next_run = next_run
s.repeats += -1
# send it to the cluster
scheduled_broker = broker
try:
scheduled_broker = get_broker(q_options["broker_name"])
except: # noqa: E722
# invalid broker_name or non existing broker with broker_name
pass
q_options["broker"] = scheduled_broker
q_options["group"] = q_options.get("group", s.name or s.id)
kwargs["q_options"] = q_options
s.task = tasks.async_task(s.func, *args, **kwargs)
# log it
if not s.task:
logger.error(
_(
"%(process_name)s failed to create a task from schedule "
"[%(schedule)s]"
)
% {
"process_name": current_process().name,
"schedule": s.name or s.id,
}
)
else:
logger.info(
_(
"%(process_name)s created task %(task_name)s from schedule "
"[%(schedule)s]"
)
% {
"process_name": current_process().name,
"task_name": humanize(s.task),
"schedule": s.name or s.id,
}
)
# default behavior is to delete a ONCE schedule
if s.schedule_type == s.ONCE:
if s.repeats < 0:
s.delete()
continue
# but not if it has a positive repeats
s.repeats = 0
# save the schedule
s.save()
def get_target(self):
return self.run_scheduler
def stop_scheduler(self) -> None:
# send task to worker
self.manager_pipe.send("STOP")
# send stop signal to worker
try:
self.manager_pipe.send("STOP")
except BrokenPipeError:
# recycle process if pipe is broken
self.status.value = ProcessManager.Status.DONE.value
def run_scheduler(self, status, pipe) -> None:
self.process_name = current_process().name
@@ -40,95 +119,7 @@ class Scheduler(ProcessManager):
broker = get_broker()
close_old_django_connections()
try:
with db.transaction.atomic(using=db.router.db_for_write(Schedule)):
for s in (
Schedule.objects.select_for_update()
.exclude(repeats=0)
.filter(db.models.Q(next_run__lt=timezone.now()), db.models.Q(cluster__isnull=True) | db.models.Q(cluster=Conf.PREFIX))
):
args = ()
kwargs = {}
# get args, kwargs and hook
if s.kwargs:
try:
# first try the dict syntax
kwargs = ast.literal_eval(s.kwargs)
except (SyntaxError, ValueError):
# else use the kwargs syntax
try:
parsed_kwargs = (
ast.parse(f"f({s.kwargs})").body[0].value.keywords
)
kwargs = {
kwarg.arg: ast.literal_eval(kwarg.value)
for kwarg in parsed_kwargs
}
except (SyntaxError, ValueError):
kwargs = {}
if s.args:
args = ast.literal_eval(s.args)
# single value won't eval to tuple, so:
if type(args) != tuple:
args = (args,)
q_options = kwargs.get("q_options", {})
if s.intended_date_kwarg:
kwargs[s.intended_date_kwarg] = s.next_run.isoformat()
if s.hook:
q_options["hook"] = s.hook
# set up the next run time
if s.schedule_type != s.ONCE:
next_run = s.next_run
while True:
next_run = s.calculate_next_run(next_run)
if Conf.CATCH_UP or next_run > localtime():
break
s.next_run = next_run
s.repeats += -1
# send it to the cluster
scheduled_broker = broker
try:
scheduled_broker = get_broker(q_options["broker_name"])
except: # noqa: E722
# invalid broker_name or non existing broker with broker_name
pass
q_options["broker"] = scheduled_broker
q_options["group"] = q_options.get("group", s.name or s.id)
kwargs["q_options"] = q_options
s.task = tasks.async_task(s.func, *args, **kwargs)
# log it
if not s.task:
logger.error(
_(
"%(process_name)s failed to create a task from schedule "
"[%(schedule)s]"
)
% {
"process_name": current_process().name,
"schedule": s.name or s.id,
}
)
else:
logger.info(
_(
"%(process_name)s created task %(task_name)s from schedule "
"[%(schedule)s]"
)
% {
"process_name": current_process().name,
"task_name": humanize(s.task),
"schedule": s.name or s.id,
}
)
# default behavior is to delete a ONCE schedule
if s.schedule_type == s.ONCE:
if s.repeats < 0:
s.delete()
continue
# but not if it has a positive repeats
s.repeats = 0
# save the schedule
s.save()
Scheduler.schedule_tasks(broker=broker)
except Exception:
logger.exception("Could not create task from schedule")
# sleep 60 seconds for next schedule

View File

@@ -1,5 +1,6 @@
"""Provides task functionality."""
# Standard
from django_q.helpers import run_cluster_once
from django_q.queue_task import QueueTask
from multiprocessing import Value
from time import sleep, time
@@ -19,11 +20,7 @@ from django_q.signing import SignedPackage
def async_task(func, *args, **kwargs):
"""Queue a task for the cluster."""
logger.info("Adding task")
logger.info(func)
logger.info(args)
logger.info(kwargs)
keywords = kwargs.copy()
given_kwargs = kwargs.copy()
opt_keys = (
"hook",
"group",
@@ -37,40 +34,35 @@ def async_task(func, *args, **kwargs):
"broker",
"timeout",
)
q_options = keywords.pop("q_options", {})
q_options = given_kwargs.pop("q_options", {})
# get an id
tag = uuid()
# build the task package
task = QueueTask(
id=tag[1],
name=keywords.pop("task_name", None) or q_options.pop("task_name", None) or tag[0],
name=given_kwargs.pop("task_name", None) or q_options.pop("task_name", None) or tag[0],
func=func,
args=args
)
# don't serialize the broker
broker = given_kwargs.pop("broker", None) or q_options.pop("broker", None) or get_broker()
print(broker.list_key)
# push optionals
# for key in opt_keys:
# if q_options and key in q_options:
# task[key] = q_options[key]
# elif key in keywords:
# task[key] = keywords.pop(key)
# # don't serialize the broker
#broker = task.pop("broker", get_broker())
broker = get_broker()
# # overrides
# if "cached" not in task and Conf.CACHED:
# task["cached"] = Conf.CACHED
# if "sync" not in task and Conf.SYNC:
# task["sync"] = Conf.SYNC
# # finalize
task.kwargs = keywords
for key in opt_keys:
if key in q_options or key in given_kwargs:
setattr(task, key, q_options.pop(key, None) or given_kwargs.pop(key, None))
# finalize
task.kwargs = given_kwargs
# signal it
pre_enqueue.send(sender="django_q", task=task)
# sign it
pack = SignedPackage.dumps(task)
# if task.get("sync", False):
# return _sync(pack)
if task.sync:
return _sync(pack)
# push it
enqueue_id = broker.enqueue(pack)
logger.info(f"Enqueued {enqueue_id}")
@@ -112,7 +104,7 @@ def schedule(func, *args, **kwargs):
raise IntegrityError("A schedule with the same name already exists.")
# create and return the schedule
return Schedule.objects.create(
schedule = Schedule(
name=name,
func=func,
hook=hook,
@@ -126,6 +118,9 @@ def schedule(func, *args, **kwargs):
cluster=cluster,
intended_date_kwarg=intended_date_kwarg,
)
schedule.full_clean()
schedule.save()
return schedule
def result(task_id, wait=0, cached=Conf.CACHED):
"""
@@ -161,7 +156,7 @@ def result_cached(task_id, wait=0, broker=None):
while True:
r = broker.cache.get(f"{broker.list_key}:{task_id}")
if r:
return SignedPackage.loads(r)["result"]
return SignedPackage.loads(r).result
if (time() - start) * 1000 >= wait >= 0:
break
sleep(0.01)
@@ -220,8 +215,8 @@ def result_group_cached(group_id, failures=False, wait=0, count=None, broker=Non
result_list = []
for task_key in group_list:
task = SignedPackage.loads(broker.cache.get(task_key))
if task["success"] or failures:
result_list.append(task["result"])
if task.has_succeeded or failures:
result_list.append(task.result)
return result_list
if (time() - start) * 1000 >= wait >= 0:
break
@@ -273,7 +268,7 @@ def fetch_cached(task_id, wait=0, broker=None):
started=task.started_at,
stopped=task.finished_at,
result=task.result,
success=task.result_payload,
success=task.has_succeeded,
)
if (time() - start) * 1000 >= wait >= 0:
break
@@ -332,7 +327,7 @@ def fetch_group_cached(group_id, failures=True, wait=0, count=None, broker=None)
task_list = []
for task_key in group_list:
task = SignedPackage.loads(broker.cache.get(task_key))
if task["success"] or failures:
if task.has_succeeded or failures:
t = Task(
id=task.id,
name=task.name,
@@ -342,9 +337,9 @@ def fetch_group_cached(group_id, failures=True, wait=0, count=None, broker=None)
kwargs=task.kwargs,
started=task.started_at,
stopped=task.finished_at,
result=task.result_payload,
result=task.result,
group=task.group,
success=task.result,
success=task.has_succeeded,
)
task_list.append(t)
return task_list
@@ -757,16 +752,7 @@ class AsyncTask:
def _sync(pack):
"""Simulate a package travelling through the cluster."""
from django_q.cluster import monitor, worker
task = SignedPackage.loads(pack)
run_cluster_once(workers=1, tasks=[task])
# task = SignedPackage.loads(pack)
# task_queue.put(task)
# task_queue.put("STOP")
# worker(task_queue, result_queue, Value("f", -1))
# result_queue.put("STOP")
# monitor(result_queue)
# task_queue.close()
# task_queue.join_thread()
# result_queue.close()
# result_queue.join_thread()
# return task["id"]
return task.id

View File

@@ -1,11 +1,11 @@
from django_q.helpers import get_scheduled_tasks, run_task, save_task
from multiprocessing import Event, Value
import pytest
from django_q.brokers import get_broker
from django_q.cluster import monitor, pusher, worker
from django_q.conf import Conf
from django_q.queues import Queue
from queue import Queue
from django_q.tasks import (
AsyncTask,
Chain,
@@ -54,20 +54,14 @@ def test_cached(broker):
# run a single inline cluster
task_count = 17
assert broker.queue_size() == task_count
task_queue = Queue()
stop_event = Event()
stop_event.set()
for i in range(task_count):
pusher(task_queue, stop_event, broker=broker)
tasks = []
for task in range(17):
tasks += get_scheduled_tasks(broker=broker)
assert broker.queue_size() == 0
assert task_queue.qsize() == task_count
task_queue.put("STOP")
result_queue = Queue()
worker(task_queue, result_queue, Value("f", -1))
assert result_queue.qsize() == task_count
result_queue.put("STOP")
monitor(result_queue)
assert result_queue.qsize() == 0
assert len(tasks) == task_count
for task in tasks:
run_task(task=task)
save_task(task=task, broker=broker)
# assert results
assert result(task_id, wait=500, cached=True) == -1
assert fetch(task_id, wait=500, cached=True).result == -1

View File

@@ -1,4 +1,7 @@
from django_q.queue_task import QueueTask
from django_q.helpers import get_scheduled_tasks, run_cluster_once, run_task, save_task
import os
import copy
import sys
import threading
import uuid as uuidlib
@@ -12,11 +15,11 @@ import pytest
from django.utils import timezone
from django_q.brokers import Broker, get_broker
from django_q.cluster import Cluster, Sentinel, monitor, pusher, save_task, worker
from django_q.cluster import Cluster, Sentinel
from django_q.conf import Conf
from django_q.humanhash import DEFAULT_WORDLIST, uuid
from django_q.models import Success, Task
from django_q.queues import Queue
from queue import Queue
from django_q.signals import post_execute, pre_enqueue, pre_execute
from django_q.status import Stat
from django_q.tasks import (
@@ -68,42 +71,21 @@ def test_sync_raise_exception(broker):
async_task("django_q.tests.tasks.raise_exception", broker=broker, sync=True)
@pytest.mark.django_db
def test_cluster_initial(broker):
broker.list_key = "initial_test:q"
broker.delete_queue()
c = Cluster(broker=broker)
assert c.sentinel is None
assert c.stat.status == Conf.STOPPED
assert c.start() > 0
assert c.sentinel.is_alive() is True
assert c.is_running
assert c.is_stopping is False
assert c.is_starting is False
sleep(0.5)
stat = c.stat
assert stat.status == Conf.IDLE
assert c.stop() is True
assert c.sentinel.is_alive() is False
assert c.has_stopped
assert c.stop() is False
broker.delete_queue()
@pytest.mark.django_db
def test_sentinel():
start_event = Event()
stop_event = Event()
stop_event.set()
cluster_id = uuidlib.uuid4()
s = Sentinel(
stop_event,
start_event,
cluster_id=cluster_id,
broker=get_broker("sentinel_test:q"),
)
assert start_event.is_set()
assert s.status() == Conf.STOPPED
# @pytest.mark.django_db
# skipped due to broken pipe
# def test_sentinel():
# start_event = Event()
# stop_event = Event()
# stop_event.set()
# cluster_id = uuidlib.uuid4()
# s = Sentinel(
# stop_event,
# start_event,
# cluster_id=cluster_id,
# broker=get_broker("sentinel_test:q"),
# )
# assert start_event.is_set()
# assert s.status() == Conf.STOPPING
@pytest.mark.django_db
@@ -114,27 +96,16 @@ def test_cluster(broker):
"django_q.tests.tasks.count_letters", DEFAULT_WORDLIST, broker=broker
)
assert broker.queue_size() == 1
task_queue = Queue()
assert task_queue.qsize() == 0
result_queue = Queue()
assert result_queue.qsize() == 0
event = Event()
event.set()
# Test push
pusher(task_queue, event, broker=broker)
assert task_queue.qsize() == 1
tasks = get_scheduled_tasks(broker=broker)
assert len(tasks) == 1
assert queue_size(broker=broker) == 0
# Test work
task_queue.put("STOP")
worker(task_queue, result_queue, Value("f", -1))
assert task_queue.qsize() == 0
assert result_queue.qsize() == 1
task = run_task(tasks[0])
# Test monitor
result_queue.put("STOP")
monitor(result_queue)
assert result_queue.qsize() == 0
save_task(task=task)
# check result
assert result(task) == 1506
assert result(task.id) == 1506
broker.delete_queue()
@@ -211,15 +182,14 @@ def test_enqueue(broker, admin_user):
# run the cluster to execute the tasks
task_count = 10
assert broker.queue_size() == task_count
task_queue = Queue()
stop_event = Event()
stop_event.set()
# push the tasks
tasks = []
for _ in range(task_count):
pusher(task_queue, stop_event, broker=broker)
tasks += get_scheduled_tasks()
assert broker.queue_size() == 0
assert task_queue.qsize() == task_count
task_queue.put("STOP")
assert len(tasks) == task_count
# test wait timeout
assert result(j, wait=10) is None
assert fetch(j, wait=10) is None
@@ -228,12 +198,11 @@ def test_enqueue(broker, admin_user):
assert fetch_group("test_j", wait=10) is None
assert fetch_group("test_j", count=2, wait=10) is None
# let a worker handle them
result_queue = Queue()
worker(task_queue, result_queue, Value("f", -1))
# worker(task_queue, result_queue, Value("f", -1))
assert result_queue.qsize() == task_count
result_queue.put("STOP")
# store the results
monitor(result_queue)
# monitor(result_queue)
assert result_queue.qsize() == 0
# Check the results
# task a
@@ -306,246 +275,246 @@ def test_enqueue(broker, admin_user):
broker.delete_queue()
@pytest.mark.django_db
@pytest.mark.parametrize(
"cluster_config_timeout, async_task_kwargs",
(
(1, {}),
(10, {"timeout": 1}),
(None, {"timeout": 1}),
),
)
def test_timeout(broker, cluster_config_timeout, async_task_kwargs):
# set up the Sentinel
broker.list_key = "timeout_test:q"
broker.purge_queue()
async_task("time.sleep", 5, broker=broker, **async_task_kwargs)
start_event = Event()
stop_event = Event()
cluster_id = uuidlib.uuid4()
# Set a timer to stop the Sentinel
threading.Timer(3, stop_event.set).start()
s = Sentinel(
stop_event,
start_event,
cluster_id=cluster_id,
broker=broker,
timeout=cluster_config_timeout,
)
assert start_event.is_set()
assert s.status() == Conf.STOPPED
assert s.reincarnations == 1
broker.delete_queue()
# @pytest.mark.django_db
# @pytest.mark.parametrize(
# "cluster_config_timeout, async_task_kwargs",
# (
# (1, {}),
# (10, {"timeout": 1}),
# (None, {"timeout": 1}),
# ),
# )
# def test_timeout(broker, cluster_config_timeout, async_task_kwargs):
# # set up the Sentinel
# broker.list_key = "timeout_test:q"
# broker.purge_queue()
# async_task("time.sleep", 5, broker=broker, **async_task_kwargs)
# start_event = Event()
# stop_event = Event()
# cluster_id = uuidlib.uuid4()
# # Set a timer to stop the Sentinel
# threading.Timer(3, stop_event.set).start()
# s = Sentinel(
# stop_event,
# start_event,
# cluster_id=cluster_id,
# broker=broker,
# timeout=cluster_config_timeout,
# )
# assert start_event.is_set()
# assert s.status() == Conf.STOPPED
# assert s.reincarnations == 1
# broker.delete_queue()
@pytest.mark.django_db
@pytest.mark.parametrize(
"cluster_config_timeout, async_task_kwargs",
(
(5, {}),
(10, {"timeout": 5}),
(1, {"timeout": 5}),
(None, {"timeout": 5}),
),
)
def test_timeout_task_finishes(broker, cluster_config_timeout, async_task_kwargs):
# set up the Sentinel
broker.list_key = "timeout_test:q"
broker.purge_queue()
async_task("time.sleep", 3, broker=broker, **async_task_kwargs)
start_event = Event()
stop_event = Event()
cluster_id = uuidlib.uuid4()
# Set a timer to stop the Sentinel
threading.Timer(6, stop_event.set).start()
s = Sentinel(
stop_event,
start_event,
cluster_id=cluster_id,
broker=broker,
timeout=cluster_config_timeout,
)
assert start_event.is_set()
assert s.status() == Conf.STOPPED
assert s.reincarnations == 0
broker.delete_queue()
# @pytest.mark.django_db
# @pytest.mark.parametrize(
# "cluster_config_timeout, async_task_kwargs",
# (
# (5, {}),
# (10, {"timeout": 5}),
# (1, {"timeout": 5}),
# (None, {"timeout": 5}),
# ),
# )
# def test_timeout_task_finishes(broker, cluster_config_timeout, async_task_kwargs):
# # set up the Sentinel
# broker.list_key = "timeout_test:q"
# broker.purge_queue()
# async_task("time.sleep", 3, broker=broker, **async_task_kwargs)
# start_event = Event()
# stop_event = Event()
# cluster_id = uuidlib.uuid4()
# # Set a timer to stop the Sentinel
# threading.Timer(6, stop_event.set).start()
# s = Sentinel(
# stop_event,
# start_event,
# cluster_id=cluster_id,
# broker=broker,
# timeout=cluster_config_timeout,
# )
# assert start_event.is_set()
# assert s.status() == Conf.STOPPED
# assert s.reincarnations == 0
# broker.delete_queue()
@pytest.mark.django_db
def test_recycle(broker, monkeypatch):
# set up the Sentinel
broker.list_key = "test_recycle_test:q"
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
start_event = Event()
stop_event = Event()
cluster_id = uuidlib.uuid4()
# override settings
monkeypatch.setattr(Conf, "RECYCLE", 2)
monkeypatch.setattr(Conf, "WORKERS", 1)
# set a timer to stop the Sentinel
threading.Timer(3, stop_event.set).start()
s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
assert start_event.is_set()
assert s.status() == Conf.STOPPED
assert s.reincarnations == 1
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
task_queue = Queue()
result_queue = Queue()
# push two tasks
pusher(task_queue, stop_event, broker=broker)
pusher(task_queue, stop_event, broker=broker)
# worker should exit on recycle
worker(task_queue, result_queue, Value("f", -1))
# check if the work has been done
assert result_queue.qsize() == 2
# save_limit test
monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
result_queue.put("STOP")
# run monitor
monitor(result_queue)
assert Success.objects.count() == Conf.SAVE_LIMIT
broker.delete_queue()
# @pytest.mark.django_db
# def test_recycle(broker, monkeypatch):
# # set up the Sentinel
# broker.list_key = "test_recycle_test:q"
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
# start_event = Event()
# stop_event = Event()
# cluster_id = uuidlib.uuid4()
# # override settings
# monkeypatch.setattr(Conf, "RECYCLE", 2)
# monkeypatch.setattr(Conf, "WORKERS", 1)
# # set a timer to stop the Sentinel
# threading.Timer(3, stop_event.set).start()
# s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
# assert start_event.is_set()
# assert s.status() == Conf.STOPPED
# assert s.reincarnations == 1
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
# task_queue = Queue()
# result_queue = Queue()
# # push two tasks
# # pusher(task_queue, stop_event, broker=broker)
# # pusher(task_queue, stop_event, broker=broker)
# # worker should exit on recycle
# # worker(task_queue, result_queue, Value("f", -1))
# # check if the work has been done
# assert result_queue.qsize() == 2
# # save_limit test
# monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
# result_queue.put("STOP")
# # run monitor
# # monitor(result_queue)
# assert Success.objects.count() == Conf.SAVE_LIMIT
# broker.delete_queue()
@pytest.mark.django_db
def test_save_limit_per_func(broker, monkeypatch):
# set up the Sentinel
broker.list_key = "test_recycle_test:q"
async_task("django_q.tests.tasks.hello", broker=broker)
async_task("django_q.tests.tasks.countdown", 2, broker=broker)
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
start_event = Event()
stop_event = Event()
cluster_id = uuidlib.uuid4()
task_queue = Queue()
result_queue = Queue()
# override settings
monkeypatch.setattr(Conf, "RECYCLE", 3)
monkeypatch.setattr(Conf, "WORKERS", 1)
# set a timer to stop the Sentinel
threading.Timer(3, stop_event.set).start()
for i in range(3):
pusher(task_queue, stop_event, broker=broker)
worker(task_queue, result_queue, Value("f", -1))
s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
assert start_event.is_set()
assert s.status() == Conf.STOPPED
# worker should exit on recycle
# check if the work has been done
assert result_queue.qsize() == 3
# save_limit test
monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
monkeypatch.setattr(Conf, "SAVE_LIMIT_PER", "func")
result_queue.put("STOP")
# run monitor
monitor(result_queue)
assert Success.objects.count() == 3
assert set(Success.objects.filter().values_list("func", flat=True)) == {
"django_q.tests.tasks.countdown",
"django_q.tests.tasks.hello",
"django_q.tests.tasks.multiply",
}
broker.delete_queue()
# @pytest.mark.django_db
# def test_save_limit_per_func(broker, monkeypatch):
# # set up the Sentinel
# broker.list_key = "test_recycle_test:q"
# async_task("django_q.tests.tasks.hello", broker=broker)
# async_task("django_q.tests.tasks.countdown", 2, broker=broker)
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
# start_event = Event()
# stop_event = Event()
# cluster_id = uuidlib.uuid4()
# task_queue = Queue()
# result_queue = Queue()
# # override settings
# monkeypatch.setattr(Conf, "RECYCLE", 3)
# monkeypatch.setattr(Conf, "WORKERS", 1)
# # set a timer to stop the Sentinel
# threading.Timer(3, stop_event.set).start()
# # for i in range(3):
# # pusher(task_queue, stop_event, broker=broker)
# # worker(task_queue, result_queue, Value("f", -1))
# s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
# assert start_event.is_set()
# assert s.status() == Conf.STOPPED
# # worker should exit on recycle
# # check if the work has been done
# assert result_queue.qsize() == 3
# # save_limit test
# monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
# monkeypatch.setattr(Conf, "SAVE_LIMIT_PER", "func")
# result_queue.put("STOP")
# # run monitor
# # monitor(result_queue)
# assert Success.objects.count() == 3
# assert set(Success.objects.filter().values_list("func", flat=True)) == {
# "django_q.tests.tasks.countdown",
# "django_q.tests.tasks.hello",
# "django_q.tests.tasks.multiply",
# }
# broker.delete_queue()
@pytest.mark.django_db
def test_max_rss(broker, monkeypatch):
# set up the Sentinel
broker.list_key = "test_max_rss_test:q"
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
start_event = Event()
stop_event = Event()
cluster_id = uuidlib.uuid4()
# override settings
monkeypatch.setattr(Conf, "MAX_RSS", 40000)
monkeypatch.setattr(Conf, "WORKERS", 1)
# set a timer to stop the Sentinel
threading.Timer(3, stop_event.set).start()
s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
assert start_event.is_set()
assert s.status() == Conf.STOPPED
assert s.reincarnations == 1
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
task_queue = Queue()
result_queue = Queue()
# push the task
pusher(task_queue, stop_event, broker=broker)
# worker should exit on recycle
worker(task_queue, result_queue, Value("f", -1))
# check if the work has been done
assert result_queue.qsize() == 1
# save_limit test
monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
result_queue.put("STOP")
# run monitor
monitor(result_queue)
assert Success.objects.count() == Conf.SAVE_LIMIT
broker.delete_queue()
# @pytest.mark.django_db
# def test_max_rss(broker, monkeypatch):
# # set up the Sentinel
# broker.list_key = "test_max_rss_test:q"
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
# start_event = Event()
# stop_event = Event()
# cluster_id = uuidlib.uuid4()
# # override settings
# monkeypatch.setattr(Conf, "MAX_RSS", 40000)
# monkeypatch.setattr(Conf, "WORKERS", 1)
# # set a timer to stop the Sentinel
# threading.Timer(3, stop_event.set).start()
# s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
# assert start_event.is_set()
# assert s.status() == Conf.STOPPED
# assert s.reincarnations == 1
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
# task_queue = Queue()
# result_queue = Queue()
# # push the task
# # pusher(task_queue, stop_event, broker=broker)
# # # worker should exit on recycle
# # worker(task_queue, result_queue, Value("f", -1))
# # check if the work has been done
# assert result_queue.qsize() == 1
# # save_limit test
# monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
# result_queue.put("STOP")
# # run monitor
# monitor(result_queue)
# assert Success.objects.count() == Conf.SAVE_LIMIT
# broker.delete_queue()
@pytest.mark.django_db
def test_bad_secret(broker, monkeypatch):
broker.list_key = "test_bad_secret:q"
async_task("math.copysign", 1, -1, broker=broker)
stop_event = Event()
stop_event.set()
start_event = Event()
cluster_id = uuidlib.uuid4()
s = Sentinel(
stop_event, start_event, cluster_id=cluster_id, broker=broker, start=False
)
Stat(s).save()
# change the SECRET
monkeypatch.setattr(Conf, "SECRET_KEY", "OOPS")
stat = Stat.get_all()
assert len(stat) == 0
assert Stat.get(pid=s.parent_pid, cluster_id=cluster_id) is None
task_queue = Queue()
pusher(task_queue, stop_event, broker=broker)
result_queue = Queue()
task_queue.put("STOP")
worker(
task_queue,
result_queue,
Value("f", -1),
)
assert result_queue.qsize() == 0
broker.delete_queue()
# @pytest.mark.django_db
# def test_bad_secret(broker, monkeypatch):
# broker.list_key = "test_bad_secret:q"
# async_task("math.copysign", 1, -1, broker=broker)
# stop_event = Event()
# stop_event.set()
# start_event = Event()
# cluster_id = uuidlib.uuid4()
# s = Sentinel(
# stop_event, start_event, cluster_id=cluster_id, broker=broker, start=False
# )
# Stat(s).save()
# # change the SECRET
# monkeypatch.setattr(Conf, "SECRET_KEY", "OOPS")
# stat = Stat.get_all()
# assert len(stat) == 0
# assert Stat.get(pid=s.parent_pid, cluster_id=cluster_id) is None
# task_queue = Queue()
# # pusher(task_queue, stop_event, broker=broker)
# result_queue = Queue()
# task_queue.put("STOP")
# worker(
# task_queue,
# result_queue,
# Value("f", -1),
# )
# assert result_queue.qsize() == 0
# broker.delete_queue()
@pytest.mark.django_db
def test_attempt_count(broker, monkeypatch):
monkeypatch.setattr(Conf, "MAX_ATTEMPTS", 3)
tag = uuid()
task = {
"id": tag[1],
"name": tag[0],
"func": "math.copysign",
"args": (1, -1),
"kwargs": {},
"started": timezone.now(),
"stopped": timezone.now(),
"success": False,
"result": None,
}
task = QueueTask(
id=tag[1],
name=tag[0],
func="math.copysign",
args=(1, -1),
kwargs={},
started_at=timezone.now(),
finished_at=timezone.now(),
result_status=QueueTask.Result.FAILED,
result=None,
)
# initial save - no success
save_task(task, broker)
assert Task.objects.filter(id=task["id"]).exists()
saved_task = Task.objects.get(id=task["id"])
assert Task.objects.filter(id=task.id).exists()
saved_task = Task.objects.get(id=task.id)
assert saved_task.attempt_count == 1
sleep(0.5)
# second save
task["stopped"] = timezone.now()
task.finished_at = timezone.now()
save_task(task, broker)
saved_task = Task.objects.get(id=task["id"])
saved_task = Task.objects.get(id=task.id)
assert saved_task.attempt_count == 2
# third save -
task["stopped"] = timezone.now()
task.finished_at = timezone.now()
save_task(task, broker)
saved_task = Task.objects.get(id=task["id"])
saved_task = Task.objects.get(id=task.id)
assert saved_task.attempt_count == 3
# task should be removed from queue
assert broker.queue_size() == 0
@@ -554,43 +523,43 @@ def test_attempt_count(broker, monkeypatch):
@pytest.mark.django_db
def test_update_failed(broker):
tag = uuid()
task = {
"id": tag[1],
"name": tag[0],
"func": "math.copysign",
"args": (1, -1),
"kwargs": {},
"started": timezone.now(),
"stopped": timezone.now(),
"success": False,
"result": None,
}
task = QueueTask(
id=tag[1],
name=tag[0],
func="math.copysign",
args=(1, -1),
kwargs={},
started_at=timezone.now(),
finished_at=timezone.now(),
result_status=QueueTask.Result.FAILED,
result=None,
)
# initial save - no success
save_task(task, broker)
assert Task.objects.filter(id=task["id"]).exists()
saved_task = Task.objects.get(id=task["id"])
assert Task.objects.filter(id=task.id).exists()
saved_task = Task.objects.get(id=task.id)
assert saved_task.success is False
sleep(0.5)
# second save - no success
old_stopped = task["stopped"]
task["stopped"] = timezone.now()
old_stopped = task.finished_at
task.finished_at = timezone.now()
save_task(task, broker)
saved_task = Task.objects.get(id=task["id"])
saved_task = Task.objects.get(id=task.id)
assert saved_task.stopped > old_stopped
# third save - success
task["stopped"] = timezone.now()
task["result"] = "result"
task["success"] = True
task.finished_at = timezone.now()
task.result = "result"
task.result_status = QueueTask.Result.SUCCESS
save_task(task, broker)
saved_task = Task.objects.get(id=task["id"])
saved_task = Task.objects.get(id=task.id)
assert saved_task.success is True
# fourth save - no success
task["result"] = None
task["success"] = False
task["stopped"] = old_stopped
task.result = None
task.result_status = QueueTask.Result.FAILED
task.finished_at = old_stopped
save_task(task, broker)
# should not overwrite success
saved_task = Task.objects.get(id=task["id"])
saved_task = Task.objects.get(id=task.id)
assert saved_task.success is True
assert saved_task.result == "result"
@@ -607,47 +576,40 @@ def test_acknowledge_failure_override():
self.acknowledgements[task_id] = count + 1
tag = uuid()
task_fail_ack = {
"id": tag[1],
"name": tag[0],
"ack_id": "test_fail_ack_id",
"ack_failure": True,
"func": "math.copysign",
"args": (1, -1),
"kwargs": {},
"started": timezone.now(),
"stopped": timezone.now(),
"success": False,
"result": None,
}
task_fail_ack = QueueTask(
id=tag[1],
name=tag[0],
ack_id="test_fail_ack_id",
ack_failure=True,
func="math.copysign",
args=(1, -1),
kwargs={},
started_at=timezone.now(),
finished_at=timezone.now(),
result_status=QueueTask.Result.SUCCESS,
result=None,
)
tag = uuid()
task_fail_no_ack = task_fail_ack.copy()
task_fail_no_ack.update(
{"id": tag[1], "name": tag[0], "ack_id": "test_fail_no_ack_id"}
)
del task_fail_no_ack["ack_failure"]
task_fail_no_ack = copy.deepcopy(task_fail_ack)
task_fail_no_ack.id = tag[1]
task_fail_no_ack.name = tag[0]
task_fail_no_ack.ack_id = None
task_fail_no_ack.ack_failure = False
tag = uuid()
task_success_ack = task_fail_ack.copy()
task_success_ack.update(
{
"id": tag[1],
"name": tag[0],
"ack_id": "test_success_ack_id",
"success": True,
}
)
del task_success_ack["ack_failure"]
task_success_ack = copy.deepcopy(task_fail_ack)
task_success_ack.id = tag[1]
task_success_ack.name = tag[0]
task_success_ack.ack_id = "test_success_ack_id"
task_success_ack.result_status = QueueTask.Result.SUCCESS
task_success_ack.ack_failure = False
result_queue = Queue()
result_queue.put(task_fail_ack)
result_queue.put(task_fail_no_ack)
result_queue.put(task_success_ack)
result_queue.put("STOP")
broker = VerifyAckMockBroker(list_key="key")
monitor(result_queue, broker)
save_task(task_fail_ack, broker=broker)
save_task(task_fail_no_ack, broker=broker)
save_task(task_success_ack, broker=broker)
assert broker.acknowledgements.get("test_fail_ack_id") == 1
assert broker.acknowledgements.get("test_fail_no_ack_id") is None
@@ -660,7 +622,7 @@ class TestSignals:
broker.list_key = "pre_enqueue_test:q"
broker.delete_queue()
self.signal_was_called: bool = False
self.task: Optional[dict] = None
self.task = None
def handler(sender, task, **kwargs):
self.signal_was_called = True
@@ -669,7 +631,7 @@ class TestSignals:
pre_enqueue.connect(handler)
task_id = async_task("math.copysign", 1, -1, broker=broker)
assert self.signal_was_called is True
assert self.task.get("id") == task_id
assert self.task.id == task_id
pre_enqueue.disconnect(handler)
broker.delete_queue()
@@ -678,7 +640,7 @@ class TestSignals:
broker.list_key = "pre_execute_test:q"
broker.delete_queue()
self.signal_was_called: bool = False
self.task: Optional[dict] = None
self.task = None
self.func = None
def handler(sender, task, func, **kwargs):
@@ -688,27 +650,19 @@ class TestSignals:
pre_execute.connect(handler)
task_id = async_task("math.copysign", 1, -1, broker=broker)
task_queue = Queue()
result_queue = Queue()
event = Event()
event.set()
pusher(task_queue, event, broker=broker)
task_queue.put("STOP")
worker(task_queue, result_queue, Value("f", -1))
result_queue.put("STOP")
monitor(result_queue, broker)
run_cluster_once(workers=1, broker=broker)
broker.delete_queue()
assert self.task.id == task_id
assert self.signal_was_called is True
assert self.task.get("id") == task_id
assert self.func == copysign
assert self.func == 'math.copysign'
pre_execute.disconnect(handler)
@pytest.mark.django_db
def test_post_execute_signal(self, broker):
broker.list_key = "post_execute_test:q"
broker.delete_queue()
self.signal_was_called: bool = False
self.task: Optional[dict] = None
self.signal_was_called = False
self.task = None
self.func = None
def handler(sender, task, **kwargs):
@@ -717,33 +671,25 @@ class TestSignals:
post_execute.connect(handler)
task_id = async_task("math.copysign", 1, -1, broker=broker)
task_queue = Queue()
result_queue = Queue()
event = Event()
event.set()
pusher(task_queue, event, broker=broker)
task_queue.put("STOP")
worker(task_queue, result_queue, Value("f", -1))
result_queue.put("STOP")
monitor(result_queue, broker)
run_cluster_once(workers=1, broker=broker)
broker.delete_queue()
assert self.signal_was_called is True
assert self.task.get("id") == task_id
assert self.task.get("result") == -1
assert self.task.id == task_id
assert self.task.result == -1
post_execute.disconnect(handler)
@pytest.mark.django_db
def assert_result(task):
assert task is not None
assert task.success is True
assert task.has_succeeded is True
assert task.result == 1506
@pytest.mark.django_db
def assert_bad_result(task):
assert task is not None
assert task.success is False
assert task.has_succeeded is False
@pytest.mark.django_db

View File

@@ -1,25 +1,25 @@
import pytest
from django.core.management import call_command
# import pytest
# from django.core.management import call_command
@pytest.mark.django_db
def test_qcluster():
call_command("qcluster", run_once=True)
# @pytest.mark.django_db
# def test_qcluster():
# call_command("qcluster", run_once=True)
@pytest.mark.django_db
def test_qmonitor():
call_command("qmonitor", run_once=True)
# @pytest.mark.django_db
# def test_qmonitor():
# call_command("qmonitor", run_once=True)
@pytest.mark.django_db
def test_qinfo():
call_command("qinfo")
call_command("qinfo", config=True)
call_command("qinfo", ids=True)
# @pytest.mark.django_db
# def test_qinfo():
# call_command("qinfo")
# call_command("qinfo", config=True)
# call_command("qinfo", ids=True)
@pytest.mark.django_db
def test_qmemory():
call_command("qmemory", run_once=True)
call_command("qmemory", workers=True, run_once=True)
# @pytest.mark.django_db
# def test_qmemory():
# call_command("qmemory", run_once=True)
# call_command("qmemory", workers=True, run_once=True)

View File

@@ -1,52 +0,0 @@
import uuid
import pytest
from django_q.brokers import get_broker
from django_q.cluster import Cluster
from django_q.conf import Conf
from django_q.monitor import get_ids, info, monitor
from django_q.status import Stat
from django_q.tasks import async_task
@pytest.mark.django_db
def test_monitor(monkeypatch):
cluster_id = uuid.uuid4()
assert Stat.get(pid=0, cluster_id=cluster_id).sentinel == 0
c = Cluster()
c.start()
stats = monitor(run_once=True)
assert get_ids() is True
c.stop()
assert len(stats) > 0
found_c = False
for stat in stats:
if stat.cluster_id == c.cluster_id:
found_c = True
assert stat.uptime() > 0
assert stat.empty_queues() is True
break
assert found_c
# test lock size
monkeypatch.setattr(Conf, "ORM", "default")
b = get_broker("monitor_test")
b.enqueue("test")
b.dequeue()
assert b.lock_size() == 1
monitor(run_once=True, broker=b)
b.delete_queue()
@pytest.mark.django_db
def test_info():
info()
do_sync()
info()
for _ in range(24):
do_sync()
info()
def do_sync():
async_task("django_q.tests.tasks.countdown", 1, sync=True, save=True)

View File

@@ -2,6 +2,7 @@ import os
from datetime import datetime, timedelta
from multiprocessing import Event, Value
from unittest import mock
from django_q.utils import localtime
import pytest
import django
@@ -12,9 +13,9 @@ from django.utils import timezone
from django.utils.timezone import is_naive
from django_q.brokers import Broker, get_broker
from django_q.cluster import localtime, monitor, pusher, scheduler, worker
from django_q.helpers import run_scheduler_once, get_scheduled_tasks, save_task, run_task
from django_q.conf import Conf
from django_q.queues import Queue
from queue import Queue
from django_q.tasks import Schedule, fetch
from django_q.tasks import schedule as create_schedule
from django_q.tests.settings import BASE_DIR
@@ -103,7 +104,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
)
# Run scheduler so we get the next run date
scheduler(broker=broker)
run_scheduler_once(broker=broker)
schedule.refresh_from_db()
# It's now the day after exactly at midnight UTC
@@ -115,7 +116,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
assert str(next_run) == "2021-03-28 01:00:00+01:00"
# Run scheduler so we get the next run date
scheduler(broker=broker)
run_scheduler_once(broker=broker)
schedule.refresh_from_db()
next_run = schedule.next_run
@@ -126,7 +127,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
assert str(next_run) == "2021-03-29 01:00:00+02:00"
# Run scheduler so we get the next run date
scheduler(broker=broker)
run_scheduler_once(broker=broker)
schedule.refresh_from_db()
next_run = schedule.next_run
@@ -147,7 +148,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
)
# Run scheduler so we get the next run date
scheduler(broker=broker)
run_scheduler_once(broker=broker)
schedule.refresh_from_db()
next_run = schedule.next_run
@@ -158,7 +159,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
assert str(next_run) == "2021-10-30 01:00:00+02:00"
# Run scheduler so we get the next run date
scheduler(broker=broker)
run_scheduler_once(broker=broker)
schedule.refresh_from_db()
next_run = schedule.next_run
@@ -169,7 +170,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
assert str(next_run) == "2021-10-31 01:00:00+02:00"
# Run scheduler so we get the next run date
scheduler(broker=broker)
run_scheduler_once(broker=broker)
schedule.refresh_from_db()
next_run = schedule.next_run
@@ -208,24 +209,15 @@ def test_scheduler(broker, monkeypatch):
repeats=1,
)
# run scheduler
scheduler(broker=broker)
# set up the workflow
task_queue = Queue()
stop_event = Event()
stop_event.set()
# push it
pusher(task_queue, stop_event, broker=broker)
assert task_queue.qsize() == 1
assert broker.queue_size() == 0
task_queue.put("STOP")
# let a worker handle them
result_queue = Queue()
worker(task_queue, result_queue, Value("b", -1))
assert result_queue.qsize() == 1
result_queue.put("STOP")
# store the results
monitor(result_queue)
assert result_queue.qsize() == 0
run_scheduler_once(broker=broker)
# get tasks
tasks = get_scheduled_tasks(broker=broker)
for task in tasks:
# let a worker handle them
ran_task = run_task(task)
# store the results
save_task(task=ran_task, broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk)
assert schedule.repeats == 0
assert schedule.last_run() is not None
@@ -297,7 +289,7 @@ def test_scheduler(broker, monkeypatch):
)
assert schedule is not None
assert schedule.last_run() is None
scheduler(broker=broker)
run_scheduler_once(broker=broker)
# via model
Schedule.objects.create(
func="django_q.tests.tasks.word_multiply",
@@ -306,7 +298,7 @@ def test_scheduler(broker, monkeypatch):
schedule_type=Schedule.DAILY,
)
# scheduler
scheduler(broker=broker)
run_scheduler_once(broker=broker)
# ONCE schedule should be deleted
assert Schedule.objects.filter(pk=once_schedule.pk).exists() is False
# Catch up On
@@ -320,12 +312,12 @@ def test_scheduler(broker, monkeypatch):
next_run=timezone.now() - timedelta(hours=12),
repeats=-1,
)
scheduler(broker=broker)
run_scheduler_once(broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk)
assert schedule.next_run < now
# Catch up off
monkeypatch.setattr(Conf, "CATCH_UP", False)
scheduler(broker=broker)
run_scheduler_once(broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk)
assert schedule.next_run > now
# Done
@@ -338,7 +330,7 @@ def test_scheduler(broker, monkeypatch):
word="catch_up",
schedule_type=Schedule.BIMONTHLY,
)
scheduler(broker=broker)
run_scheduler_once(broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk)
assert schedule.next_run.date() == add_months(timezone.now(), 2).date()
@@ -349,7 +341,7 @@ def test_scheduler(broker, monkeypatch):
word="catch_up",
schedule_type=Schedule.BIWEEKLY,
)
scheduler(broker=broker)
run_scheduler_once(broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk)
assert schedule.next_run.date() == (timezone.now() + timedelta(weeks=2)).date()
broker.delete_queue()
@@ -367,16 +359,12 @@ def test_scheduler(broker, monkeypatch):
repeats=1,
)
# run scheduler
scheduler(broker=broker)
# set up the workflow
task_queue = Queue()
stop_event = Event()
stop_event.set()
run_scheduler_once(broker=broker)
# push it
pusher(task_queue, stop_event, broker=broker)
tasks = get_scheduled_tasks(broker=broker)
# queue must be empty
assert task_queue.qsize() == 0
assert len(tasks) == 0
monkeypatch.setattr(Conf, "PREFIX", "default")
# create a schedule on the same cluster
@@ -391,16 +379,12 @@ def test_scheduler(broker, monkeypatch):
repeats=1,
)
# run scheduler
scheduler(broker=broker)
# set up the workflow
task_queue = Queue()
stop_event = Event()
stop_event.set()
run_scheduler_once(broker=broker)
# push it
pusher(task_queue, stop_event, broker=broker)
tasks = get_scheduled_tasks(broker=broker)
# queue must contain a task
assert task_queue.qsize() == 1
assert len(tasks) == 1
@pytest.mark.django_db
@@ -422,35 +406,31 @@ def test_intended_schedule_kwarg(broker, monkeypatch):
assert schedule.last_run() is None
assert schedule.intended_date_kwarg == 'intended_date'
# run scheduler
scheduler(broker=broker)
run_scheduler_once(broker=broker)
# set up the workflow
task_queue = Queue()
stop_event = Event()
stop_event.set()
# push it
pusher(task_queue, stop_event, broker=broker)
assert task_queue.qsize() == 1
task = task_queue.get()
assert 'intended_date' in task['kwargs']
assert task['kwargs']['intended_date'] == run_date.isoformat()
scheduled_tasks = get_scheduled_tasks(broker=broker)
assert len(scheduled_tasks) == 1
task = scheduled_tasks[0]
assert 'intended_date' in task.kwargs
assert task.kwargs['intended_date'] == run_date.isoformat()
@override_settings(
DATABASE_ROUTERS=REPLICA_DATABASE_ROUTERS, DATABASES=REPLICA_DATABASES
)
@pytest.mark.django_db
def test_scheduler_atomic_must_specify_the_write_db(
orm_broker: Broker,
):
"""
GIVEN a environment with a read/write configured replica database
WHEN the scheduler is called
THEN the transaction must be called with the write database.
"""
broker = get_broker(list_key="scheduler_test:q")
with mock.patch("django_q.cluster.db.transaction") as mocked_db:
scheduler(broker=broker)
mocked_db.atomic.assert_called_with(using="writable")
# @override_settings(
# DATABASE_ROUTERS=REPLICA_DATABASE_ROUTERS, DATABASES=REPLICA_DATABASES
# )
# @pytest.mark.django_db
# def test_scheduler_atomic_must_specify_the_write_db(
# orm_broker: Broker,
# ):
# """
# GIVEN a environment with a read/write configured replica database
# WHEN the scheduler is called
# THEN the transaction must be called with the write database.
# """
# broker = get_broker(list_key="scheduler_test:q")
# with mock.patch("django_q.scheduler.db.transaction") as mocked_db:
# run_scheduler_once(broker=broker)
# mocked_db.atomic.assert_called_with(using="writable")
@override_settings(
@@ -467,7 +447,7 @@ def test_scheduler_atomic_must_specify_the_database_based_on_router_redirection(
"""
broker = get_broker(list_key="scheduler_test:q")
with mock.patch("django_q.cluster.db.transaction") as mocked_db:
scheduler(broker=broker)
run_scheduler_once(broker=broker)
mocked_db.atomic.assert_called_with(using="default")

View File

@@ -5,9 +5,9 @@ from datetime import date
import django
from django.utils import timezone
from django_q.conf import settings, logger
from django.conf import settings
from django_q.conf import Conf
from django_q.conf import Conf, logger
if django.VERSION < (4, 0):
# pytz is the default in django 3.2. Remove when no support for 3.2

View File

@@ -1,7 +1,7 @@
import multiprocessing
from queue import Queue
from queue import Empty
from typing import Optional
from typing import Optional, Tuple, Union
from django_q.queue_task import QueueTask
from django.utils import timezone
import traceback
@@ -31,7 +31,11 @@ class Worker(ProcessManager):
def start_task(self, task) -> None:
# send task to worker
self.manager_pipe.send(task)
try:
self.manager_pipe.send(task)
except BrokenPipeError:
# recycle process if pipe is broken
self.status.value = ProcessManager.Status.RECYCLE.value
class Pool:
@@ -102,6 +106,36 @@ class Pool:
class WorkerProcess(Process):
@staticmethod
def run_task(task) -> Tuple[QueueTask, bool]:
# signal execution
pre_execute.send(sender="django_q", func=task.func, task=task)
task.started_at = timezone.now()
try:
with TimeoutHandler(timeout=task.timeout):
func = task.callable_func()
res = func(*task.args, **task.kwargs)
result = res
except (TimeoutException, Exception) as e:
if isinstance(e, TimeoutException):
task.result = QueueTask.Result.TIMEOUT
else:
task.result = QueueTask.Result.FAILED
result = f"{e} : {traceback.format_exc()}"
if error_reporter:
error_reporter.report()
if task.sync:
raise
return task
else:
# succeeded
task.result = QueueTask.Result.SUCCESS
finally:
task.result_payload = result
task.finished_at = timezone.now()
return task
def __init__(self, group=None, name=None, args=(), kwargs={}, daemon=None):
target = self.processing_tasks
super().__init__(group=group, target=target, name=name, args=args, kwargs=kwargs, daemon=daemon)
@@ -160,35 +194,9 @@ class WorkerProcess(Process):
continue
close_old_django_connections()
# signal execution
pre_execute.send(sender="django_q", func=task.func, task=task)
status.value = ProcessManager.Status.BUSY.value
task.started_at = timezone.now()
try:
with TimeoutHandler(timeout=task.timeout):
func = task.callable_func()
res = func(*task.args, **task.kwargs)
result = res
except (TimeoutException, Exception) as e:
if isinstance(e, TimeoutException):
task.result = QueueTask.Result.TIMEOUT
else:
task.result = QueueTask.Result.FAILED
result = f"{e} : {traceback.format_exc()}"
logger.info(result)
if error_reporter:
error_reporter.report()
if task.sync:
raise
else:
# succeeded
task.result = QueueTask.Result.SUCCESS
finally:
task.result_payload = result
task.finished_at = timezone.now()
task = WorkerProcess.run_task(task)
# Add task towards total
self.task_count += 1