diff --git a/django_toosimple_q/admin.py b/django_toosimple_q/admin.py index ec8dca6..435e3bd 100644 --- a/django_toosimple_q/admin.py +++ b/django_toosimple_q/admin.py @@ -188,6 +188,10 @@ def last_due_(self, obj): @admin.display() def next_due_(self, obj): + # for schedule not in the code anymore + if not obj.schedule: + return "invalid" + if len(obj.past_dues) >= 1: next_due = obj.past_dues[0] else: diff --git a/django_toosimple_q/management/commands/worker.py b/django_toosimple_q/management/commands/worker.py index 740e27b..c9ec60c 100644 --- a/django_toosimple_q/management/commands/worker.py +++ b/django_toosimple_q/management/commands/worker.py @@ -2,6 +2,7 @@ import logging import os import signal +from time import sleep from traceback import format_exc from django.core.management.base import BaseCommand, CommandError @@ -65,10 +66,10 @@ def add_arguments(self, parser): def handle(self, *args, **options): # Handle interuption signals signal.signal(signal.SIGINT, self.handle_signal) - # signal.signal(signal.SIGTERM, self.handle_signal) - signal.signal(signal.SIGTERM, signal.default_int_handler) - # for simulating an exception in tests + signal.signal(signal.SIGTERM, self.handle_signal) + # Custom signal to provoke an artifical exception, used for testing only if hasattr(signal, "SIGUSR1"): + # this doesn't exist on Windows signal.signal(signal.SIGUSR1, self.handle_signal) # TODO: replace by simple-parsing @@ -243,7 +244,7 @@ def do_loop(self) -> bool: logger.debug(f"Waiting for next tick...") next_run = last_run + datetime.timedelta(seconds=self.tick_duration) while not self.exit_requested and now() < next_run: - pass + sleep(1) return True diff --git a/django_toosimple_q/models.py b/django_toosimple_q/models.py index d1c15d7..293a7e2 100644 --- a/django_toosimple_q/models.py +++ b/django_toosimple_q/models.py @@ -204,6 +204,10 @@ def icon(self): @cached_property def past_dues(self): + if self.schedule is None: + # Deal with invalid schedule (e.g. deleted from the code but still in the DB) + return [] + if self.schedule.cron == "manual": # A manual schedule is never due return [] diff --git a/django_toosimple_q/schedule.py b/django_toosimple_q/schedule.py index a246cbd..789b4fc 100644 --- a/django_toosimple_q/schedule.py +++ b/django_toosimple_q/schedule.py @@ -2,7 +2,6 @@ from typing import Dict, List, Optional from .logging import logger -from .registry import tasks_registry from .task import Task @@ -42,9 +41,7 @@ def execute(self, dues: List[Optional[datetime]]): if self.datetime_kwarg: dt_kwarg = {self.datetime_kwarg: due} - tasks_registry[self.name].enqueue( - *self.args, due=due, **dt_kwarg, **self.kwargs - ) + self.task.enqueue(*self.args, due=due, **dt_kwarg, **self.kwargs) def __str__(self): return f"Schedule {self.name}" diff --git a/django_toosimple_q/tests/tests_regression.py b/django_toosimple_q/tests/tests_regression.py index cb51aee..998b440 100644 --- a/django_toosimple_q/tests/tests_regression.py +++ b/django_toosimple_q/tests/tests_regression.py @@ -1,11 +1,15 @@ import time +from django.core import management + +from django_toosimple_q.decorators import register_task, schedule_task from django_toosimple_q.models import TaskExec +from django_toosimple_q.registry import schedules_registry, tasks_registry -from .base import TooSimpleQBackgroundTestCase +from .base import TooSimpleQBackgroundTestCase, TooSimpleQRegularTestCase -class TestRegression(TooSimpleQBackgroundTestCase): +class TestRegressionBackground(TooSimpleQBackgroundTestCase): def test_regr_schedule_short(self): # Regression test for an issue where a schedule with smaller periods was not always processed @@ -17,3 +21,32 @@ def test_regr_schedule_short(self): # It should do almost 20 tasks self.assertGreaterEqual(TaskExec.objects.all().count(), 18) + + +class TestRegressionRegular(TooSimpleQRegularTestCase): + def test_deleting_schedule(self): + # Regression test for an issue where deleting a schedule in code would crash the admin view + + @schedule_task(cron="0 12 * * *", datetime_kwarg="scheduled_on") + @register_task(name="normal") + def a(scheduled_on): + return f"{scheduled_on:%Y-%m-%d %H:%M}" + + management.call_command("worker", "--until_done") + + schedules_registry.clear() + tasks_registry.clear() + + # the admin view still works even for deleted schedules + response = self.client.get("/admin/toosimpleq/scheduleexec/") + self.assertEqual(response.status_code, 200) + + def test_different_schedule_and_task(self): + # Regression test for an issue where schedule with a different name than the task would fail + + @schedule_task(cron="0 12 * * *", name="name_a", run_on_creation=True) + @register_task(name="name_b") + def a(): + return True + + management.call_command("worker", "--until_done")