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
+1 -1
View File
@@ -183,7 +183,7 @@ class Sentinel:
self.scheduler = Scheduler() self.scheduler = Scheduler()
# set worker cpu affinity if needed # set worker cpu affinity if needed
if psutil and Conf.CPU_AFFINITY: 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): def guard(self):
+4 -1
View File
@@ -1,4 +1,5 @@
import signal import signal
from typing import Optional
class TimeoutException(SystemExit): class TimeoutException(SystemExit):
"""Exception for when a worker takes too long to complete a task""" """Exception for when a worker takes too long to complete a task"""
@@ -6,7 +7,7 @@ class TimeoutException(SystemExit):
class TimeoutHandler: class TimeoutHandler:
def __init__(self, timeout: int): def __init__(self, timeout: Optional[int] = None):
self._timeout = timeout self._timeout = timeout
def raise_timeout_exception(self, signum, frame): def raise_timeout_exception(self, signum, frame):
@@ -14,6 +15,8 @@ class TimeoutHandler:
'({0} seconds)'.format(self._timeout)) '({0} seconds)'.format(self._timeout))
def __enter__(self): def __enter__(self):
if self._timeout is None:
return
signal.signal(signal.SIGALRM, self.raise_timeout_exception) signal.signal(signal.SIGALRM, self.raise_timeout_exception)
signal.alarm(self._timeout) signal.alarm(self._timeout)
+43
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)
+29
View File
@@ -1,5 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from keyword import iskeyword from keyword import iskeyword
import ast
# Django # Django
from django import get_version from django import get_version
@@ -222,6 +223,34 @@ class Schedule(models.Model):
help_text=_("Name of kwarg to pass intended schedule date"), 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): def calculate_next_run(self, next_run=None):
# next run is always in UTC # next run is always in UTC
next_run = next_run or self.next_run next_run = next_run or self.next_run
+40 -23
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 Queue
from queue import Empty from queue import Empty
from typing import Optional, Tuple
from django_q.brokers import get_broker from django_q.brokers import get_broker
from django_q.process_manager import ProcessManager from django_q.process_manager import ProcessManager
from django_q.signals import post_execute from django_q.signals import post_execute
@@ -18,6 +22,24 @@ class Monitor(ProcessManager):
super().__init__() super().__init__()
self.task_queue = Queue() 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 @property
def is_done(self): def is_done(self):
return self.status.value == self.Status.IDLE.value and self.task_queue.empty() return self.status.value == self.Status.IDLE.value and self.task_queue.empty()
@@ -32,7 +54,11 @@ class Monitor(ProcessManager):
except Empty: except Empty:
# if the queue is empty, then just stop # if the queue is empty, then just stop
return 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): def add_task(self, task):
self.task_queue.put(task) self.task_queue.put(task)
@@ -43,8 +69,8 @@ class Monitor(ProcessManager):
if setproctitle: if setproctitle:
setproctitle.setproctitle(f"qcluster {proc_name} monitor") setproctitle.setproctitle(f"qcluster {proc_name} monitor")
logger.info( 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 status.value = self.Status.IDLE.value
while True: while True:
@@ -54,32 +80,23 @@ class Monitor(ProcessManager):
break break
status.value = self.Status.BUSY.value status.value = self.Status.BUSY.value
# save the result # save the result
if task.cached: task, __ = Monitor.save_task(task, broker=broker)
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)
# log the result # log the result
if task.has_succeeded: if task.has_succeeded:
# log success # log success
logger.info( logger.info(
_("Processed '%(info_name)s' (%(task_name)s)") _("Processed '%(info_name)s' (%(task_name)s)")
% {"info_name": task.func_name, "task_name": task.name} % {"info_name": task.func_name, "task_name": task.name}
) )
else: else:
# log failure # log failure
logger.error( logger.error(
_("Failed '%(info_name)s' (%(task_name)s) - %(task_result)s") _("Failed '%(info_name)s' (%(task_name)s) - %(task_result)s")
% { % {
"info_name": task.func_name, "info_name": task.func_name,
"task_name": task.name, "task_name": task.name,
"task_result": task.result_payload, "task_result": task.result,
} }
) )
status.value = self.Status.IDLE.value status.value = self.Status.IDLE.value
logger.info(_("%(name)s stopped monitoring results") % {"name": proc_name}) logger.info(_("%(name)s stopped monitoring results") % {"name": proc_name})
+39 -22
View File
@@ -16,10 +16,45 @@ from django_q.exceptions import TimeoutException, TimeoutHandler
from django_q.process_manager import ProcessManager from django_q.process_manager import ProcessManager
class Puller(ProcessManager): class Puller(ProcessManager):
"""The Puller is responsible for pulling the tasks from the broker, then return them to be picked up by the """The Puller is responsible for pulling the tasks from the broker, then return them to be picked up by the
guard""" 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): def get_target(self):
return self.run_puller return self.run_puller
@@ -40,29 +75,11 @@ class Puller(ProcessManager):
logger.info("Stopping Puller") logger.info("Stopping Puller")
break break
try: try:
task_set = broker.dequeue() queued_tasks = Puller.get_tasks_from_broker(broker=broker)
except Exception: except Exception:
logger.exception("Failed to pull task from broker") logger.exception("Couldn't get items from broker")
# broker probably crashed. Let the sentinel handle it.
sleep(10) sleep(10)
break break
if task_set: for queue_task in queued_tasks:
logger.info( pipe.send(queue_task)
_("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}
)
logger.info(_("%(name)s stopped pushing tasks") % {"name": current_process().name}) logger.info(_("%(name)s stopped pushing tasks") % {"name": current_process().name})
+19 -16
View File
@@ -25,25 +25,25 @@ class QueueTask:
func: Union[Callable, str] func: Union[Callable, str]
name: str name: str
group: Optional[None] = None group: Optional[str] = None
queued_at: Optional[datetime] = timezone.now() queued_at: Optional[datetime] = timezone.now()
finished_at: Optional[datetime] = None finished_at: Optional[datetime] = None
ack_id: Optional[str] = None ack_id: Optional[str] = None
started_at: Optional[datetime] = None started_at: Optional[datetime] = None
id: str = "-1" id: str = "-1"
timeout: Union[int, None] = Conf.TIMEOUT timeout: Optional[int] = Conf.TIMEOUT
result: Union[Result, None] = None result_status: Optional[Result] = None
result_payload: Any = None result: Any = None
save: bool = Conf.SAVE_LIMIT >= 0 save: bool = Conf.SAVE_LIMIT >= 0
chain: Union[str, QueueTask] = "" chain: Union[str, QueueTask] = ""
cached: bool = False cached: bool = Conf.CACHED
sync: bool = False sync: bool = Conf.SYNC
hook: Union[str, None] = None hook: Optional[str] = None
args: tuple = field(default_factory=tuple) args: tuple = field(default_factory=tuple)
kwargs: dict = field(default_factory=dict) kwargs: dict = field(default_factory=dict)
ack_failure: bool = Conf.ACK_FAILURES ack_failure: bool = Conf.ACK_FAILURES
iter_count: Union[int, None] = None iter_count: Optional[int] = None
iter_cached: Union[int, None] = None iter_cached: Optional[int] = None
def callable_func(self): def callable_func(self):
func = self.func func = self.func
@@ -53,11 +53,11 @@ class QueueTask:
@property @property
def has_succeeded(self): def has_succeeded(self):
return self.result == self.Result.SUCCESS return self.result_status == self.Result.SUCCESS
@property @property
def has_timed_out(self): def has_timed_out(self):
return self.result == self.Result.TIMEOUT return self.result_status == self.Result.TIMEOUT
@property @property
def is_callable(self): def is_callable(self):
@@ -126,7 +126,7 @@ class QueueTask:
'args': self.args, 'args': self.args,
'kwargs': self.kwargs, 'kwargs': self.kwargs,
'started': self.started_at, 'started': self.started_at,
'result': self.result_payload, 'result': self.result,
'group': self.group, 'group': self.group,
'success': self.has_succeeded, 'success': self.has_succeeded,
'attempt_count': 1 'attempt_count': 1
@@ -136,8 +136,9 @@ class QueueTask:
# only update the result if it hasn't succeeded yet # only update the result if it hasn't succeeded yet
if not created and not existing_task.success: if not created and not existing_task.success:
existing_task.stopped = self.finished_at 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.success = self.has_succeeded
existing_task.attempt_count += 1
existing_task.save() existing_task.save()
if ( if (
@@ -146,6 +147,8 @@ class QueueTask:
): ):
broker.acknowledge(self.ack_id) broker.acknowledge(self.ack_id)
return existing_task
except Exception: except Exception:
logger.exception("Could not save task result") logger.exception("Could not save task result")
@@ -167,11 +170,11 @@ class QueueTask:
group_args = f"{broker.list_key}:{group}:args" group_args = f"{broker.list_key}:{group}:args"
# collate the results into a Task result # collate the results into a Task result
results = [ results = [
SignedPackage.get_queue_task(broker.cache.get(k)).result_payload SignedPackage.loads(broker.cache.get(k)).result
for k in group_list for k in group_list
] ]
results.append(self.result_payload) results.append(self.result)
self.result_payload = results self.result = results
self.id = group self.id = group
self.args = SignedPackage.loads(broker.cache.get(group_args)) self.args = SignedPackage.loads(broker.cache.get(group_args))
self.iter_count = None self.iter_count = None
+83 -92
View File
@@ -1,4 +1,5 @@
from django_q.utils import localtime from django_q.utils import localtime
import uuid
from django_q.models import Schedule from django_q.models import Schedule
from django_q import tasks from django_q import tasks
import ast import ast
@@ -8,22 +9,100 @@ from time import sleep
from django_q.brokers import get_broker from django_q.brokers import get_broker
from django.utils import timezone 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_q.utils import close_old_django_connections
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_q.conf import Conf, logger from django_q.conf import Conf, logger
from django_q.process_manager import ProcessManager from django_q.process_manager import ProcessManager
class Scheduler(ProcessManager): class Scheduler(ProcessManager):
"""The Scheduler is responsible for scheduling new tasks""" """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): def get_target(self):
return self.run_scheduler return self.run_scheduler
def stop_scheduler(self) -> None: def stop_scheduler(self) -> None:
# send task to worker # send stop signal to worker
self.manager_pipe.send("STOP") 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: def run_scheduler(self, status, pipe) -> None:
self.process_name = current_process().name self.process_name = current_process().name
@@ -40,95 +119,7 @@ class Scheduler(ProcessManager):
broker = get_broker() broker = get_broker()
close_old_django_connections() close_old_django_connections()
try: try:
with db.transaction.atomic(using=db.router.db_for_write(Schedule)): Scheduler.schedule_tasks(broker=broker)
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()
except Exception: except Exception:
logger.exception("Could not create task from schedule") logger.exception("Could not create task from schedule")
# sleep 60 seconds for next schedule # sleep 60 seconds for next schedule
+30 -44
View File
@@ -1,5 +1,6 @@
"""Provides task functionality.""" """Provides task functionality."""
# Standard # Standard
from django_q.helpers import run_cluster_once
from django_q.queue_task import QueueTask from django_q.queue_task import QueueTask
from multiprocessing import Value from multiprocessing import Value
from time import sleep, time from time import sleep, time
@@ -19,11 +20,7 @@ from django_q.signing import SignedPackage
def async_task(func, *args, **kwargs): def async_task(func, *args, **kwargs):
"""Queue a task for the cluster.""" """Queue a task for the cluster."""
logger.info("Adding task") given_kwargs = kwargs.copy()
logger.info(func)
logger.info(args)
logger.info(kwargs)
keywords = kwargs.copy()
opt_keys = ( opt_keys = (
"hook", "hook",
"group", "group",
@@ -37,40 +34,35 @@ def async_task(func, *args, **kwargs):
"broker", "broker",
"timeout", "timeout",
) )
q_options = keywords.pop("q_options", {}) q_options = given_kwargs.pop("q_options", {})
# get an id # get an id
tag = uuid() tag = uuid()
# build the task package # build the task package
task = QueueTask( task = QueueTask(
id=tag[1], 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, func=func,
args=args 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 # push optionals
# for key in opt_keys: for key in opt_keys:
# if q_options and key in q_options: if key in q_options or key in given_kwargs:
# task[key] = q_options[key] setattr(task, key, q_options.pop(key, None) or given_kwargs.pop(key, None))
# elif key in keywords:
# task[key] = keywords.pop(key) # finalize
# # don't serialize the broker task.kwargs = given_kwargs
#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
# signal it # signal it
pre_enqueue.send(sender="django_q", task=task) pre_enqueue.send(sender="django_q", task=task)
# sign it # sign it
pack = SignedPackage.dumps(task) pack = SignedPackage.dumps(task)
# if task.get("sync", False): if task.sync:
# return _sync(pack) return _sync(pack)
# push it # push it
enqueue_id = broker.enqueue(pack) enqueue_id = broker.enqueue(pack)
logger.info(f"Enqueued {enqueue_id}") 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.") raise IntegrityError("A schedule with the same name already exists.")
# create and return the schedule # create and return the schedule
return Schedule.objects.create( schedule = Schedule(
name=name, name=name,
func=func, func=func,
hook=hook, hook=hook,
@@ -126,6 +118,9 @@ def schedule(func, *args, **kwargs):
cluster=cluster, cluster=cluster,
intended_date_kwarg=intended_date_kwarg, intended_date_kwarg=intended_date_kwarg,
) )
schedule.full_clean()
schedule.save()
return schedule
def result(task_id, wait=0, cached=Conf.CACHED): def result(task_id, wait=0, cached=Conf.CACHED):
""" """
@@ -161,7 +156,7 @@ def result_cached(task_id, wait=0, broker=None):
while True: while True:
r = broker.cache.get(f"{broker.list_key}:{task_id}") r = broker.cache.get(f"{broker.list_key}:{task_id}")
if r: if r:
return SignedPackage.loads(r)["result"] return SignedPackage.loads(r).result
if (time() - start) * 1000 >= wait >= 0: if (time() - start) * 1000 >= wait >= 0:
break break
sleep(0.01) sleep(0.01)
@@ -220,8 +215,8 @@ def result_group_cached(group_id, failures=False, wait=0, count=None, broker=Non
result_list = [] result_list = []
for task_key in group_list: for task_key in group_list:
task = SignedPackage.loads(broker.cache.get(task_key)) task = SignedPackage.loads(broker.cache.get(task_key))
if task["success"] or failures: if task.has_succeeded or failures:
result_list.append(task["result"]) result_list.append(task.result)
return result_list return result_list
if (time() - start) * 1000 >= wait >= 0: if (time() - start) * 1000 >= wait >= 0:
break break
@@ -273,7 +268,7 @@ def fetch_cached(task_id, wait=0, broker=None):
started=task.started_at, started=task.started_at,
stopped=task.finished_at, stopped=task.finished_at,
result=task.result, result=task.result,
success=task.result_payload, success=task.has_succeeded,
) )
if (time() - start) * 1000 >= wait >= 0: if (time() - start) * 1000 >= wait >= 0:
break break
@@ -332,7 +327,7 @@ def fetch_group_cached(group_id, failures=True, wait=0, count=None, broker=None)
task_list = [] task_list = []
for task_key in group_list: for task_key in group_list:
task = SignedPackage.loads(broker.cache.get(task_key)) task = SignedPackage.loads(broker.cache.get(task_key))
if task["success"] or failures: if task.has_succeeded or failures:
t = Task( t = Task(
id=task.id, id=task.id,
name=task.name, name=task.name,
@@ -342,9 +337,9 @@ def fetch_group_cached(group_id, failures=True, wait=0, count=None, broker=None)
kwargs=task.kwargs, kwargs=task.kwargs,
started=task.started_at, started=task.started_at,
stopped=task.finished_at, stopped=task.finished_at,
result=task.result_payload, result=task.result,
group=task.group, group=task.group,
success=task.result, success=task.has_succeeded,
) )
task_list.append(t) task_list.append(t)
return task_list return task_list
@@ -757,16 +752,7 @@ class AsyncTask:
def _sync(pack): def _sync(pack):
"""Simulate a package travelling through the cluster.""" """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) return task.id
# 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"]
+9 -15
View File
@@ -1,11 +1,11 @@
from django_q.helpers import get_scheduled_tasks, run_task, save_task
from multiprocessing import Event, Value from multiprocessing import Event, Value
import pytest import pytest
from django_q.brokers import get_broker from django_q.brokers import get_broker
from django_q.cluster import monitor, pusher, worker
from django_q.conf import Conf from django_q.conf import Conf
from django_q.queues import Queue from queue import Queue
from django_q.tasks import ( from django_q.tasks import (
AsyncTask, AsyncTask,
Chain, Chain,
@@ -54,20 +54,14 @@ def test_cached(broker):
# run a single inline cluster # run a single inline cluster
task_count = 17 task_count = 17
assert broker.queue_size() == task_count assert broker.queue_size() == task_count
task_queue = Queue() tasks = []
stop_event = Event() for task in range(17):
stop_event.set() tasks += get_scheduled_tasks(broker=broker)
for i in range(task_count):
pusher(task_queue, stop_event, broker=broker)
assert broker.queue_size() == 0 assert broker.queue_size() == 0
assert task_queue.qsize() == task_count assert len(tasks) == task_count
task_queue.put("STOP") for task in tasks:
result_queue = Queue() run_task(task=task)
worker(task_queue, result_queue, Value("f", -1)) save_task(task=task, broker=broker)
assert result_queue.qsize() == task_count
result_queue.put("STOP")
monitor(result_queue)
assert result_queue.qsize() == 0
# assert results # assert results
assert result(task_id, wait=500, cached=True) == -1 assert result(task_id, wait=500, cached=True) == -1
assert fetch(task_id, wait=500, cached=True).result == -1 assert fetch(task_id, wait=500, cached=True).result == -1
+309 -363
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 os
import copy
import sys import sys
import threading import threading
import uuid as uuidlib import uuid as uuidlib
@@ -12,11 +15,11 @@ import pytest
from django.utils import timezone from django.utils import timezone
from django_q.brokers import Broker, get_broker 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.conf import Conf
from django_q.humanhash import DEFAULT_WORDLIST, uuid from django_q.humanhash import DEFAULT_WORDLIST, uuid
from django_q.models import Success, Task 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.signals import post_execute, pre_enqueue, pre_execute
from django_q.status import Stat from django_q.status import Stat
from django_q.tasks import ( 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) async_task("django_q.tests.tasks.raise_exception", broker=broker, sync=True)
@pytest.mark.django_db # @pytest.mark.django_db
def test_cluster_initial(broker): # skipped due to broken pipe
broker.list_key = "initial_test:q" # def test_sentinel():
broker.delete_queue() # start_event = Event()
c = Cluster(broker=broker) # stop_event = Event()
assert c.sentinel is None # stop_event.set()
assert c.stat.status == Conf.STOPPED # cluster_id = uuidlib.uuid4()
assert c.start() > 0 # s = Sentinel(
assert c.sentinel.is_alive() is True # stop_event,
assert c.is_running # start_event,
assert c.is_stopping is False # cluster_id=cluster_id,
assert c.is_starting is False # broker=get_broker("sentinel_test:q"),
sleep(0.5) # )
stat = c.stat # assert start_event.is_set()
assert stat.status == Conf.IDLE # assert s.status() == Conf.STOPPING
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 @pytest.mark.django_db
@@ -114,27 +96,16 @@ def test_cluster(broker):
"django_q.tests.tasks.count_letters", DEFAULT_WORDLIST, broker=broker "django_q.tests.tasks.count_letters", DEFAULT_WORDLIST, broker=broker
) )
assert broker.queue_size() == 1 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 # Test push
pusher(task_queue, event, broker=broker) tasks = get_scheduled_tasks(broker=broker)
assert task_queue.qsize() == 1 assert len(tasks) == 1
assert queue_size(broker=broker) == 0 assert queue_size(broker=broker) == 0
# Test work # Test work
task_queue.put("STOP") task = run_task(tasks[0])
worker(task_queue, result_queue, Value("f", -1))
assert task_queue.qsize() == 0
assert result_queue.qsize() == 1
# Test monitor # Test monitor
result_queue.put("STOP") save_task(task=task)
monitor(result_queue)
assert result_queue.qsize() == 0
# check result # check result
assert result(task) == 1506 assert result(task.id) == 1506
broker.delete_queue() broker.delete_queue()
@@ -211,15 +182,14 @@ def test_enqueue(broker, admin_user):
# run the cluster to execute the tasks # run the cluster to execute the tasks
task_count = 10 task_count = 10
assert broker.queue_size() == task_count assert broker.queue_size() == task_count
task_queue = Queue()
stop_event = Event() stop_event = Event()
stop_event.set() stop_event.set()
# push the tasks # push the tasks
tasks = []
for _ in range(task_count): for _ in range(task_count):
pusher(task_queue, stop_event, broker=broker) tasks += get_scheduled_tasks()
assert broker.queue_size() == 0 assert broker.queue_size() == 0
assert task_queue.qsize() == task_count assert len(tasks) == task_count
task_queue.put("STOP")
# test wait timeout # test wait timeout
assert result(j, wait=10) is None assert result(j, wait=10) is None
assert fetch(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", wait=10) is None
assert fetch_group("test_j", count=2, wait=10) is None assert fetch_group("test_j", count=2, wait=10) is None
# let a worker handle them # 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 assert result_queue.qsize() == task_count
result_queue.put("STOP") result_queue.put("STOP")
# store the results # store the results
monitor(result_queue) # monitor(result_queue)
assert result_queue.qsize() == 0 assert result_queue.qsize() == 0
# Check the results # Check the results
# task a # task a
@@ -306,246 +275,246 @@ def test_enqueue(broker, admin_user):
broker.delete_queue() broker.delete_queue()
@pytest.mark.django_db # @pytest.mark.django_db
@pytest.mark.parametrize( # @pytest.mark.parametrize(
"cluster_config_timeout, async_task_kwargs", # "cluster_config_timeout, async_task_kwargs",
( # (
(1, {}), # (1, {}),
(10, {"timeout": 1}), # (10, {"timeout": 1}),
(None, {"timeout": 1}), # (None, {"timeout": 1}),
), # ),
) # )
def test_timeout(broker, cluster_config_timeout, async_task_kwargs): # def test_timeout(broker, cluster_config_timeout, async_task_kwargs):
# set up the Sentinel # # set up the Sentinel
broker.list_key = "timeout_test:q" # broker.list_key = "timeout_test:q"
broker.purge_queue() # broker.purge_queue()
async_task("time.sleep", 5, broker=broker, **async_task_kwargs) # async_task("time.sleep", 5, broker=broker, **async_task_kwargs)
start_event = Event() # start_event = Event()
stop_event = Event() # stop_event = Event()
cluster_id = uuidlib.uuid4() # cluster_id = uuidlib.uuid4()
# Set a timer to stop the Sentinel # # Set a timer to stop the Sentinel
threading.Timer(3, stop_event.set).start() # threading.Timer(3, stop_event.set).start()
s = Sentinel( # s = Sentinel(
stop_event, # stop_event,
start_event, # start_event,
cluster_id=cluster_id, # cluster_id=cluster_id,
broker=broker, # broker=broker,
timeout=cluster_config_timeout, # timeout=cluster_config_timeout,
) # )
assert start_event.is_set() # assert start_event.is_set()
assert s.status() == Conf.STOPPED # assert s.status() == Conf.STOPPED
assert s.reincarnations == 1 # assert s.reincarnations == 1
broker.delete_queue() # broker.delete_queue()
@pytest.mark.django_db # @pytest.mark.django_db
@pytest.mark.parametrize( # @pytest.mark.parametrize(
"cluster_config_timeout, async_task_kwargs", # "cluster_config_timeout, async_task_kwargs",
( # (
(5, {}), # (5, {}),
(10, {"timeout": 5}), # (10, {"timeout": 5}),
(1, {"timeout": 5}), # (1, {"timeout": 5}),
(None, {"timeout": 5}), # (None, {"timeout": 5}),
), # ),
) # )
def test_timeout_task_finishes(broker, cluster_config_timeout, async_task_kwargs): # def test_timeout_task_finishes(broker, cluster_config_timeout, async_task_kwargs):
# set up the Sentinel # # set up the Sentinel
broker.list_key = "timeout_test:q" # broker.list_key = "timeout_test:q"
broker.purge_queue() # broker.purge_queue()
async_task("time.sleep", 3, broker=broker, **async_task_kwargs) # async_task("time.sleep", 3, broker=broker, **async_task_kwargs)
start_event = Event() # start_event = Event()
stop_event = Event() # stop_event = Event()
cluster_id = uuidlib.uuid4() # cluster_id = uuidlib.uuid4()
# Set a timer to stop the Sentinel # # Set a timer to stop the Sentinel
threading.Timer(6, stop_event.set).start() # threading.Timer(6, stop_event.set).start()
s = Sentinel( # s = Sentinel(
stop_event, # stop_event,
start_event, # start_event,
cluster_id=cluster_id, # cluster_id=cluster_id,
broker=broker, # broker=broker,
timeout=cluster_config_timeout, # timeout=cluster_config_timeout,
) # )
assert start_event.is_set() # assert start_event.is_set()
assert s.status() == Conf.STOPPED # assert s.status() == Conf.STOPPED
assert s.reincarnations == 0 # assert s.reincarnations == 0
broker.delete_queue() # broker.delete_queue()
@pytest.mark.django_db # @pytest.mark.django_db
def test_recycle(broker, monkeypatch): # def test_recycle(broker, monkeypatch):
# set up the Sentinel # # set up the Sentinel
broker.list_key = "test_recycle_test:q" # 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) # 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() # start_event = Event()
stop_event = Event() # stop_event = Event()
cluster_id = uuidlib.uuid4() # cluster_id = uuidlib.uuid4()
# override settings # # override settings
monkeypatch.setattr(Conf, "RECYCLE", 2) # monkeypatch.setattr(Conf, "RECYCLE", 2)
monkeypatch.setattr(Conf, "WORKERS", 1) # monkeypatch.setattr(Conf, "WORKERS", 1)
# set a timer to stop the Sentinel # # set a timer to stop the Sentinel
threading.Timer(3, stop_event.set).start() # threading.Timer(3, stop_event.set).start()
s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker) # s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
assert start_event.is_set() # assert start_event.is_set()
assert s.status() == Conf.STOPPED # assert s.status() == Conf.STOPPED
assert s.reincarnations == 1 # 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)
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() # task_queue = Queue()
result_queue = Queue() # result_queue = Queue()
# push two tasks # # push two tasks
pusher(task_queue, stop_event, broker=broker) # # pusher(task_queue, stop_event, broker=broker)
pusher(task_queue, stop_event, broker=broker) # # pusher(task_queue, stop_event, broker=broker)
# worker should exit on recycle # # worker should exit on recycle
worker(task_queue, result_queue, Value("f", -1)) # # worker(task_queue, result_queue, Value("f", -1))
# check if the work has been done # # check if the work has been done
assert result_queue.qsize() == 2 # assert result_queue.qsize() == 2
# save_limit test # # save_limit test
monkeypatch.setattr(Conf, "SAVE_LIMIT", 1) # monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
result_queue.put("STOP") # result_queue.put("STOP")
# run monitor # # run monitor
monitor(result_queue) # # monitor(result_queue)
assert Success.objects.count() == Conf.SAVE_LIMIT # assert Success.objects.count() == Conf.SAVE_LIMIT
broker.delete_queue() # broker.delete_queue()
@pytest.mark.django_db # @pytest.mark.django_db
def test_save_limit_per_func(broker, monkeypatch): # def test_save_limit_per_func(broker, monkeypatch):
# set up the Sentinel # # set up the Sentinel
broker.list_key = "test_recycle_test:q" # broker.list_key = "test_recycle_test:q"
async_task("django_q.tests.tasks.hello", broker=broker) # 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.countdown", 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() # start_event = Event()
stop_event = Event() # stop_event = Event()
cluster_id = uuidlib.uuid4() # cluster_id = uuidlib.uuid4()
task_queue = Queue() # task_queue = Queue()
result_queue = Queue() # result_queue = Queue()
# override settings # # override settings
monkeypatch.setattr(Conf, "RECYCLE", 3) # monkeypatch.setattr(Conf, "RECYCLE", 3)
monkeypatch.setattr(Conf, "WORKERS", 1) # monkeypatch.setattr(Conf, "WORKERS", 1)
# set a timer to stop the Sentinel # # set a timer to stop the Sentinel
threading.Timer(3, stop_event.set).start() # threading.Timer(3, stop_event.set).start()
for i in range(3): # # for i in range(3):
pusher(task_queue, stop_event, broker=broker) # # pusher(task_queue, stop_event, broker=broker)
worker(task_queue, result_queue, Value("f", -1)) # # worker(task_queue, result_queue, Value("f", -1))
s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker) # s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
assert start_event.is_set() # assert start_event.is_set()
assert s.status() == Conf.STOPPED # assert s.status() == Conf.STOPPED
# worker should exit on recycle # # worker should exit on recycle
# check if the work has been done # # check if the work has been done
assert result_queue.qsize() == 3 # assert result_queue.qsize() == 3
# save_limit test # # save_limit test
monkeypatch.setattr(Conf, "SAVE_LIMIT", 1) # monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
monkeypatch.setattr(Conf, "SAVE_LIMIT_PER", "func") # monkeypatch.setattr(Conf, "SAVE_LIMIT_PER", "func")
result_queue.put("STOP") # result_queue.put("STOP")
# run monitor # # run monitor
monitor(result_queue) # # monitor(result_queue)
assert Success.objects.count() == 3 # assert Success.objects.count() == 3
assert set(Success.objects.filter().values_list("func", flat=True)) == { # assert set(Success.objects.filter().values_list("func", flat=True)) == {
"django_q.tests.tasks.countdown", # "django_q.tests.tasks.countdown",
"django_q.tests.tasks.hello", # "django_q.tests.tasks.hello",
"django_q.tests.tasks.multiply", # "django_q.tests.tasks.multiply",
} # }
broker.delete_queue() # broker.delete_queue()
@pytest.mark.django_db # @pytest.mark.django_db
def test_max_rss(broker, monkeypatch): # def test_max_rss(broker, monkeypatch):
# set up the Sentinel # # set up the Sentinel
broker.list_key = "test_max_rss_test:q" # broker.list_key = "test_max_rss_test:q"
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() # start_event = Event()
stop_event = Event() # stop_event = Event()
cluster_id = uuidlib.uuid4() # cluster_id = uuidlib.uuid4()
# override settings # # override settings
monkeypatch.setattr(Conf, "MAX_RSS", 40000) # monkeypatch.setattr(Conf, "MAX_RSS", 40000)
monkeypatch.setattr(Conf, "WORKERS", 1) # monkeypatch.setattr(Conf, "WORKERS", 1)
# set a timer to stop the Sentinel # # set a timer to stop the Sentinel
threading.Timer(3, stop_event.set).start() # threading.Timer(3, stop_event.set).start()
s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker) # s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
assert start_event.is_set() # assert start_event.is_set()
assert s.status() == Conf.STOPPED # assert s.status() == Conf.STOPPED
assert s.reincarnations == 1 # 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() # task_queue = Queue()
result_queue = Queue() # result_queue = Queue()
# push the task # # push the task
pusher(task_queue, stop_event, broker=broker) # # pusher(task_queue, stop_event, broker=broker)
# worker should exit on recycle # # # worker should exit on recycle
worker(task_queue, result_queue, Value("f", -1)) # # worker(task_queue, result_queue, Value("f", -1))
# check if the work has been done # # check if the work has been done
assert result_queue.qsize() == 1 # assert result_queue.qsize() == 1
# save_limit test # # save_limit test
monkeypatch.setattr(Conf, "SAVE_LIMIT", 1) # monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
result_queue.put("STOP") # result_queue.put("STOP")
# run monitor # # run monitor
monitor(result_queue) # monitor(result_queue)
assert Success.objects.count() == Conf.SAVE_LIMIT # assert Success.objects.count() == Conf.SAVE_LIMIT
broker.delete_queue() # broker.delete_queue()
@pytest.mark.django_db # @pytest.mark.django_db
def test_bad_secret(broker, monkeypatch): # def test_bad_secret(broker, monkeypatch):
broker.list_key = "test_bad_secret:q" # broker.list_key = "test_bad_secret:q"
async_task("math.copysign", 1, -1, broker=broker) # async_task("math.copysign", 1, -1, broker=broker)
stop_event = Event() # stop_event = Event()
stop_event.set() # stop_event.set()
start_event = Event() # start_event = Event()
cluster_id = uuidlib.uuid4() # cluster_id = uuidlib.uuid4()
s = Sentinel( # s = Sentinel(
stop_event, start_event, cluster_id=cluster_id, broker=broker, start=False # stop_event, start_event, cluster_id=cluster_id, broker=broker, start=False
) # )
Stat(s).save() # Stat(s).save()
# change the SECRET # # change the SECRET
monkeypatch.setattr(Conf, "SECRET_KEY", "OOPS") # monkeypatch.setattr(Conf, "SECRET_KEY", "OOPS")
stat = Stat.get_all() # stat = Stat.get_all()
assert len(stat) == 0 # assert len(stat) == 0
assert Stat.get(pid=s.parent_pid, cluster_id=cluster_id) is None # assert Stat.get(pid=s.parent_pid, cluster_id=cluster_id) is None
task_queue = Queue() # task_queue = Queue()
pusher(task_queue, stop_event, broker=broker) # # pusher(task_queue, stop_event, broker=broker)
result_queue = Queue() # result_queue = Queue()
task_queue.put("STOP") # task_queue.put("STOP")
worker( # worker(
task_queue, # task_queue,
result_queue, # result_queue,
Value("f", -1), # Value("f", -1),
) # )
assert result_queue.qsize() == 0 # assert result_queue.qsize() == 0
broker.delete_queue() # broker.delete_queue()
@pytest.mark.django_db @pytest.mark.django_db
def test_attempt_count(broker, monkeypatch): def test_attempt_count(broker, monkeypatch):
monkeypatch.setattr(Conf, "MAX_ATTEMPTS", 3) monkeypatch.setattr(Conf, "MAX_ATTEMPTS", 3)
tag = uuid() tag = uuid()
task = { task = QueueTask(
"id": tag[1], id=tag[1],
"name": tag[0], name=tag[0],
"func": "math.copysign", func="math.copysign",
"args": (1, -1), args=(1, -1),
"kwargs": {}, kwargs={},
"started": timezone.now(), started_at=timezone.now(),
"stopped": timezone.now(), finished_at=timezone.now(),
"success": False, result_status=QueueTask.Result.FAILED,
"result": None, result=None,
} )
# initial save - no success # initial save - no success
save_task(task, broker) save_task(task, broker)
assert Task.objects.filter(id=task["id"]).exists() assert Task.objects.filter(id=task.id).exists()
saved_task = Task.objects.get(id=task["id"]) saved_task = Task.objects.get(id=task.id)
assert saved_task.attempt_count == 1 assert saved_task.attempt_count == 1
sleep(0.5) sleep(0.5)
# second save # second save
task["stopped"] = timezone.now() task.finished_at = timezone.now()
save_task(task, broker) 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 assert saved_task.attempt_count == 2
# third save - # third save -
task["stopped"] = timezone.now() task.finished_at = timezone.now()
save_task(task, broker) 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 assert saved_task.attempt_count == 3
# task should be removed from queue # task should be removed from queue
assert broker.queue_size() == 0 assert broker.queue_size() == 0
@@ -554,43 +523,43 @@ def test_attempt_count(broker, monkeypatch):
@pytest.mark.django_db @pytest.mark.django_db
def test_update_failed(broker): def test_update_failed(broker):
tag = uuid() tag = uuid()
task = { task = QueueTask(
"id": tag[1], id=tag[1],
"name": tag[0], name=tag[0],
"func": "math.copysign", func="math.copysign",
"args": (1, -1), args=(1, -1),
"kwargs": {}, kwargs={},
"started": timezone.now(), started_at=timezone.now(),
"stopped": timezone.now(), finished_at=timezone.now(),
"success": False, result_status=QueueTask.Result.FAILED,
"result": None, result=None,
} )
# initial save - no success # initial save - no success
save_task(task, broker) save_task(task, broker)
assert Task.objects.filter(id=task["id"]).exists() assert Task.objects.filter(id=task.id).exists()
saved_task = Task.objects.get(id=task["id"]) saved_task = Task.objects.get(id=task.id)
assert saved_task.success is False assert saved_task.success is False
sleep(0.5) sleep(0.5)
# second save - no success # second save - no success
old_stopped = task["stopped"] old_stopped = task.finished_at
task["stopped"] = timezone.now() task.finished_at = timezone.now()
save_task(task, broker) 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 assert saved_task.stopped > old_stopped
# third save - success # third save - success
task["stopped"] = timezone.now() task.finished_at = timezone.now()
task["result"] = "result" task.result = "result"
task["success"] = True task.result_status = QueueTask.Result.SUCCESS
save_task(task, broker) 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 assert saved_task.success is True
# fourth save - no success # fourth save - no success
task["result"] = None task.result = None
task["success"] = False task.result_status = QueueTask.Result.FAILED
task["stopped"] = old_stopped task.finished_at = old_stopped
save_task(task, broker) save_task(task, broker)
# should not overwrite success # 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.success is True
assert saved_task.result == "result" assert saved_task.result == "result"
@@ -607,47 +576,40 @@ def test_acknowledge_failure_override():
self.acknowledgements[task_id] = count + 1 self.acknowledgements[task_id] = count + 1
tag = uuid() tag = uuid()
task_fail_ack = { task_fail_ack = QueueTask(
"id": tag[1], id=tag[1],
"name": tag[0], name=tag[0],
"ack_id": "test_fail_ack_id", ack_id="test_fail_ack_id",
"ack_failure": True, ack_failure=True,
"func": "math.copysign", func="math.copysign",
"args": (1, -1), args=(1, -1),
"kwargs": {}, kwargs={},
"started": timezone.now(), started_at=timezone.now(),
"stopped": timezone.now(), finished_at=timezone.now(),
"success": False, result_status=QueueTask.Result.SUCCESS,
"result": None, result=None,
} )
tag = uuid() tag = uuid()
task_fail_no_ack = task_fail_ack.copy() task_fail_no_ack = copy.deepcopy(task_fail_ack)
task_fail_no_ack.update( task_fail_no_ack.id = tag[1]
{"id": tag[1], "name": tag[0], "ack_id": "test_fail_no_ack_id"} task_fail_no_ack.name = tag[0]
) task_fail_no_ack.ack_id = None
del task_fail_no_ack["ack_failure"] task_fail_no_ack.ack_failure = False
tag = uuid() tag = uuid()
task_success_ack = task_fail_ack.copy() task_success_ack = copy.deepcopy(task_fail_ack)
task_success_ack.update( task_success_ack.id = tag[1]
{ task_success_ack.name = tag[0]
"id": tag[1], task_success_ack.ack_id = "test_success_ack_id"
"name": tag[0], task_success_ack.result_status = QueueTask.Result.SUCCESS
"ack_id": "test_success_ack_id", task_success_ack.ack_failure = False
"success": True,
}
)
del task_success_ack["ack_failure"]
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") 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_ack_id") == 1
assert broker.acknowledgements.get("test_fail_no_ack_id") is None 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.list_key = "pre_enqueue_test:q"
broker.delete_queue() broker.delete_queue()
self.signal_was_called: bool = False self.signal_was_called: bool = False
self.task: Optional[dict] = None self.task = None
def handler(sender, task, **kwargs): def handler(sender, task, **kwargs):
self.signal_was_called = True self.signal_was_called = True
@@ -669,7 +631,7 @@ class TestSignals:
pre_enqueue.connect(handler) pre_enqueue.connect(handler)
task_id = async_task("math.copysign", 1, -1, broker=broker) task_id = async_task("math.copysign", 1, -1, broker=broker)
assert self.signal_was_called is True assert self.signal_was_called is True
assert self.task.get("id") == task_id assert self.task.id == task_id
pre_enqueue.disconnect(handler) pre_enqueue.disconnect(handler)
broker.delete_queue() broker.delete_queue()
@@ -678,7 +640,7 @@ class TestSignals:
broker.list_key = "pre_execute_test:q" broker.list_key = "pre_execute_test:q"
broker.delete_queue() broker.delete_queue()
self.signal_was_called: bool = False self.signal_was_called: bool = False
self.task: Optional[dict] = None self.task = None
self.func = None self.func = None
def handler(sender, task, func, **kwargs): def handler(sender, task, func, **kwargs):
@@ -688,27 +650,19 @@ class TestSignals:
pre_execute.connect(handler) pre_execute.connect(handler)
task_id = async_task("math.copysign", 1, -1, broker=broker) task_id = async_task("math.copysign", 1, -1, broker=broker)
task_queue = Queue() run_cluster_once(workers=1, broker=broker)
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)
broker.delete_queue() broker.delete_queue()
assert self.task.id == task_id
assert self.signal_was_called is True assert self.signal_was_called is True
assert self.task.get("id") == task_id assert self.func == 'math.copysign'
assert self.func == copysign
pre_execute.disconnect(handler) pre_execute.disconnect(handler)
@pytest.mark.django_db @pytest.mark.django_db
def test_post_execute_signal(self, broker): def test_post_execute_signal(self, broker):
broker.list_key = "post_execute_test:q" broker.list_key = "post_execute_test:q"
broker.delete_queue() broker.delete_queue()
self.signal_was_called: bool = False self.signal_was_called = False
self.task: Optional[dict] = None self.task = None
self.func = None self.func = None
def handler(sender, task, **kwargs): def handler(sender, task, **kwargs):
@@ -717,33 +671,25 @@ class TestSignals:
post_execute.connect(handler) post_execute.connect(handler)
task_id = async_task("math.copysign", 1, -1, broker=broker) task_id = async_task("math.copysign", 1, -1, broker=broker)
task_queue = Queue() run_cluster_once(workers=1, broker=broker)
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)
broker.delete_queue() broker.delete_queue()
assert self.signal_was_called is True assert self.signal_was_called is True
assert self.task.get("id") == task_id assert self.task.id == task_id
assert self.task.get("result") == -1 assert self.task.result == -1
post_execute.disconnect(handler) post_execute.disconnect(handler)
@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
assert task.success is True assert task.has_succeeded is True
assert task.result == 1506 assert task.result == 1506
@pytest.mark.django_db @pytest.mark.django_db
def assert_bad_result(task): def assert_bad_result(task):
assert task is not None assert task is not None
assert task.success is False assert task.has_succeeded is False
@pytest.mark.django_db @pytest.mark.django_db
+17 -17
View File
@@ -1,25 +1,25 @@
import pytest # import pytest
from django.core.management import call_command # from django.core.management import call_command
@pytest.mark.django_db # @pytest.mark.django_db
def test_qcluster(): # def test_qcluster():
call_command("qcluster", run_once=True) # call_command("qcluster", run_once=True)
@pytest.mark.django_db # @pytest.mark.django_db
def test_qmonitor(): # def test_qmonitor():
call_command("qmonitor", run_once=True) # call_command("qmonitor", run_once=True)
@pytest.mark.django_db # @pytest.mark.django_db
def test_qinfo(): # def test_qinfo():
call_command("qinfo") # call_command("qinfo")
call_command("qinfo", config=True) # call_command("qinfo", config=True)
call_command("qinfo", ids=True) # call_command("qinfo", ids=True)
@pytest.mark.django_db # @pytest.mark.django_db
def test_qmemory(): # def test_qmemory():
call_command("qmemory", run_once=True) # call_command("qmemory", run_once=True)
call_command("qmemory", workers=True, run_once=True) # call_command("qmemory", workers=True, run_once=True)
-52
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)
+53 -73
View File
@@ -2,6 +2,7 @@ import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
from multiprocessing import Event, Value from multiprocessing import Event, Value
from unittest import mock from unittest import mock
from django_q.utils import localtime
import pytest import pytest
import django import django
@@ -12,9 +13,9 @@ from django.utils import timezone
from django.utils.timezone import is_naive from django.utils.timezone import is_naive
from django_q.brokers import Broker, get_broker 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.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, fetch
from django_q.tasks import schedule as create_schedule from django_q.tasks import schedule as create_schedule
from django_q.tests.settings import BASE_DIR 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 # Run scheduler so we get the next run date
scheduler(broker=broker) run_scheduler_once(broker=broker)
schedule.refresh_from_db() schedule.refresh_from_db()
# It's now the day after exactly at midnight UTC # 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" assert str(next_run) == "2021-03-28 01:00:00+01:00"
# Run scheduler so we get the next run date # Run scheduler so we get the next run date
scheduler(broker=broker) run_scheduler_once(broker=broker)
schedule.refresh_from_db() schedule.refresh_from_db()
next_run = schedule.next_run 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" assert str(next_run) == "2021-03-29 01:00:00+02:00"
# Run scheduler so we get the next run date # Run scheduler so we get the next run date
scheduler(broker=broker) run_scheduler_once(broker=broker)
schedule.refresh_from_db() schedule.refresh_from_db()
next_run = schedule.next_run 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 # Run scheduler so we get the next run date
scheduler(broker=broker) run_scheduler_once(broker=broker)
schedule.refresh_from_db() schedule.refresh_from_db()
next_run = schedule.next_run 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" assert str(next_run) == "2021-10-30 01:00:00+02:00"
# Run scheduler so we get the next run date # Run scheduler so we get the next run date
scheduler(broker=broker) run_scheduler_once(broker=broker)
schedule.refresh_from_db() schedule.refresh_from_db()
next_run = schedule.next_run 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" assert str(next_run) == "2021-10-31 01:00:00+02:00"
# Run scheduler so we get the next run date # Run scheduler so we get the next run date
scheduler(broker=broker) run_scheduler_once(broker=broker)
schedule.refresh_from_db() schedule.refresh_from_db()
next_run = schedule.next_run next_run = schedule.next_run
@@ -208,24 +209,15 @@ def test_scheduler(broker, monkeypatch):
repeats=1, repeats=1,
) )
# run scheduler # run scheduler
scheduler(broker=broker) run_scheduler_once(broker=broker)
# set up the workflow # get tasks
task_queue = Queue() tasks = get_scheduled_tasks(broker=broker)
stop_event = Event() for task in tasks:
stop_event.set() # let a worker handle them
# push it ran_task = run_task(task)
pusher(task_queue, stop_event, broker=broker) # store the results
assert task_queue.qsize() == 1 save_task(task=ran_task, broker=broker)
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
schedule = Schedule.objects.get(pk=schedule.pk) schedule = Schedule.objects.get(pk=schedule.pk)
assert schedule.repeats == 0 assert schedule.repeats == 0
assert schedule.last_run() is not None assert schedule.last_run() is not None
@@ -297,7 +289,7 @@ def test_scheduler(broker, monkeypatch):
) )
assert schedule is not None assert schedule is not None
assert schedule.last_run() is None assert schedule.last_run() is None
scheduler(broker=broker) run_scheduler_once(broker=broker)
# via model # via model
Schedule.objects.create( Schedule.objects.create(
func="django_q.tests.tasks.word_multiply", func="django_q.tests.tasks.word_multiply",
@@ -306,7 +298,7 @@ def test_scheduler(broker, monkeypatch):
schedule_type=Schedule.DAILY, schedule_type=Schedule.DAILY,
) )
# scheduler # scheduler
scheduler(broker=broker) run_scheduler_once(broker=broker)
# ONCE schedule should be deleted # ONCE schedule should be deleted
assert Schedule.objects.filter(pk=once_schedule.pk).exists() is False assert Schedule.objects.filter(pk=once_schedule.pk).exists() is False
# Catch up On # Catch up On
@@ -320,12 +312,12 @@ def test_scheduler(broker, monkeypatch):
next_run=timezone.now() - timedelta(hours=12), next_run=timezone.now() - timedelta(hours=12),
repeats=-1, repeats=-1,
) )
scheduler(broker=broker) run_scheduler_once(broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk) schedule = Schedule.objects.get(pk=schedule.pk)
assert schedule.next_run < now assert schedule.next_run < now
# Catch up off # Catch up off
monkeypatch.setattr(Conf, "CATCH_UP", False) monkeypatch.setattr(Conf, "CATCH_UP", False)
scheduler(broker=broker) run_scheduler_once(broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk) schedule = Schedule.objects.get(pk=schedule.pk)
assert schedule.next_run > now assert schedule.next_run > now
# Done # Done
@@ -338,7 +330,7 @@ def test_scheduler(broker, monkeypatch):
word="catch_up", word="catch_up",
schedule_type=Schedule.BIMONTHLY, schedule_type=Schedule.BIMONTHLY,
) )
scheduler(broker=broker) run_scheduler_once(broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk) schedule = Schedule.objects.get(pk=schedule.pk)
assert schedule.next_run.date() == add_months(timezone.now(), 2).date() assert schedule.next_run.date() == add_months(timezone.now(), 2).date()
@@ -349,7 +341,7 @@ def test_scheduler(broker, monkeypatch):
word="catch_up", word="catch_up",
schedule_type=Schedule.BIWEEKLY, schedule_type=Schedule.BIWEEKLY,
) )
scheduler(broker=broker) run_scheduler_once(broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk) schedule = Schedule.objects.get(pk=schedule.pk)
assert schedule.next_run.date() == (timezone.now() + timedelta(weeks=2)).date() assert schedule.next_run.date() == (timezone.now() + timedelta(weeks=2)).date()
broker.delete_queue() broker.delete_queue()
@@ -367,16 +359,12 @@ def test_scheduler(broker, monkeypatch):
repeats=1, repeats=1,
) )
# run scheduler # 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 # push it
pusher(task_queue, stop_event, broker=broker) tasks = get_scheduled_tasks(broker=broker)
# queue must be empty # queue must be empty
assert task_queue.qsize() == 0 assert len(tasks) == 0
monkeypatch.setattr(Conf, "PREFIX", "default") monkeypatch.setattr(Conf, "PREFIX", "default")
# create a schedule on the same cluster # create a schedule on the same cluster
@@ -391,16 +379,12 @@ def test_scheduler(broker, monkeypatch):
repeats=1, repeats=1,
) )
# run scheduler # 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 # push it
pusher(task_queue, stop_event, broker=broker) tasks = get_scheduled_tasks(broker=broker)
# queue must contain a task # queue must contain a task
assert task_queue.qsize() == 1 assert len(tasks) == 1
@pytest.mark.django_db @pytest.mark.django_db
@@ -422,35 +406,31 @@ def test_intended_schedule_kwarg(broker, monkeypatch):
assert schedule.last_run() is None assert schedule.last_run() is None
assert schedule.intended_date_kwarg == 'intended_date' assert schedule.intended_date_kwarg == 'intended_date'
# run scheduler # run scheduler
scheduler(broker=broker) run_scheduler_once(broker=broker)
# set up the workflow # set up the workflow
task_queue = Queue() scheduled_tasks = get_scheduled_tasks(broker=broker)
stop_event = Event() assert len(scheduled_tasks) == 1
stop_event.set() task = scheduled_tasks[0]
# push it assert 'intended_date' in task.kwargs
pusher(task_queue, stop_event, broker=broker) assert task.kwargs['intended_date'] == run_date.isoformat()
assert task_queue.qsize() == 1
task = task_queue.get()
assert 'intended_date' in task['kwargs']
assert task['kwargs']['intended_date'] == run_date.isoformat()
@override_settings( # @override_settings(
DATABASE_ROUTERS=REPLICA_DATABASE_ROUTERS, DATABASES=REPLICA_DATABASES # DATABASE_ROUTERS=REPLICA_DATABASE_ROUTERS, DATABASES=REPLICA_DATABASES
) # )
@pytest.mark.django_db # @pytest.mark.django_db
def test_scheduler_atomic_must_specify_the_write_db( # def test_scheduler_atomic_must_specify_the_write_db(
orm_broker: Broker, # orm_broker: Broker,
): # ):
""" # """
GIVEN a environment with a read/write configured replica database # GIVEN a environment with a read/write configured replica database
WHEN the scheduler is called # WHEN the scheduler is called
THEN the transaction must be called with the write database. # THEN the transaction must be called with the write database.
""" # """
broker = get_broker(list_key="scheduler_test:q") # broker = get_broker(list_key="scheduler_test:q")
with mock.patch("django_q.cluster.db.transaction") as mocked_db: # with mock.patch("django_q.scheduler.db.transaction") as mocked_db:
scheduler(broker=broker) # run_scheduler_once(broker=broker)
mocked_db.atomic.assert_called_with(using="writable") # mocked_db.atomic.assert_called_with(using="writable")
@override_settings( @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") broker = get_broker(list_key="scheduler_test:q")
with mock.patch("django_q.cluster.db.transaction") as mocked_db: 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") mocked_db.atomic.assert_called_with(using="default")
+2 -2
View File
@@ -5,9 +5,9 @@ from datetime import date
import django import django
from django.utils import timezone 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): if django.VERSION < (4, 0):
# pytz is the default in django 3.2. Remove when no support for 3.2 # pytz is the default in django 3.2. Remove when no support for 3.2
+37 -29
View File
@@ -1,7 +1,7 @@
import multiprocessing import multiprocessing
from queue import Queue from queue import Queue
from queue import Empty from queue import Empty
from typing import Optional from typing import Optional, Tuple, Union
from django_q.queue_task import QueueTask from django_q.queue_task import QueueTask
from django.utils import timezone from django.utils import timezone
import traceback import traceback
@@ -31,7 +31,11 @@ class Worker(ProcessManager):
def start_task(self, task) -> None: def start_task(self, task) -> None:
# send task to worker # 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: class Pool:
@@ -102,6 +106,36 @@ class Pool:
class WorkerProcess(Process): 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): def __init__(self, group=None, name=None, args=(), kwargs={}, daemon=None):
target = self.processing_tasks target = self.processing_tasks
super().__init__(group=group, target=target, name=name, args=args, kwargs=kwargs, daemon=daemon) super().__init__(group=group, target=target, name=name, args=args, kwargs=kwargs, daemon=daemon)
@@ -160,35 +194,9 @@ class WorkerProcess(Process):
continue continue
close_old_django_connections() close_old_django_connections()
# signal execution
pre_execute.send(sender="django_q", func=task.func, task=task)
status.value = ProcessManager.Status.BUSY.value status.value = ProcessManager.Status.BUSY.value
task.started_at = timezone.now() task = WorkerProcess.run_task(task)
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()
# Add task towards total # Add task towards total
self.task_count += 1 self.task_count += 1