Skip to content

Commit a0fbaba

Browse files
committed
feat(schedulers): add StrictDatabaseScheduler that disables unknown tasks
Adds a new DatabaseScheduler subclass that, at startup, disables any enabled PeriodicTask whose dotted task name is not registered with the running Celery app. This addresses the common operational pain point where tasks are removed from the codebase but their database rows linger with enabled=True, causing beat to keep dispatching them and workers to log NotRegistered errors indefinitely. The new scheduler is exposed both as a class (django_celery_beat.schedulers:StrictDatabaseScheduler) and as a celery beat-scheduler entry point alias (django_strict). Disabled rows are not deleted; they can be re-enabled from the admin if the task name becomes registered again.
1 parent 99f7bcf commit a0fbaba

4 files changed

Lines changed: 84 additions & 0 deletions

File tree

django_celery_beat/schedulers.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,30 @@ def schedule(self):
542542
repr(entry) for entry in self._schedule.values()),
543543
)
544544
return self._schedule
545+
546+
547+
class StrictDatabaseScheduler(DatabaseScheduler):
548+
"""A :class:`DatabaseScheduler` that disables any periodic task whose
549+
Celery task name is not registered with the running app.
550+
551+
Useful in deployments where periodic tasks may be removed
552+
from the codebase while their database rows linger with
553+
``enabled=True``. With the default :class:`DatabaseScheduler`, beat
554+
keeps dispatching those tasks and workers raise ``NotRegistered``
555+
indefinitely. With this scheduler, such tasks are disabled at
556+
startup so they stop being dispatched.
557+
"""
558+
559+
def setup_schedule(self):
560+
super().setup_schedule()
561+
self._disable_unknown_tasks()
562+
563+
def _disable_unknown_tasks(self):
564+
for task in self.Model.objects.enabled():
565+
if task.task not in self.app.tasks:
566+
warning(
567+
'Disabling unregistered periodic task %r (task=%r)',
568+
task.name, task.task,
569+
)
570+
task.enabled = False
571+
task.save()

docs/includes/introduction.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,31 @@ with only one command (recommended for **development environment only**):
228228
3. Now you can add and manage your periodic tasks from the Django Admin interface.
229229

230230

231+
Choosing a scheduler
232+
~~~~~~~~~~~~~~~~~~~~
233+
234+
This package ships two scheduler classes. Pick based on whether the
235+
beat process has the same task registry as the workers that consume
236+
its messages:
237+
238+
- ``django_celery_beat.schedulers:StrictDatabaseScheduler``
239+
(alias ``django_strict``): **recommended when all periodic tasks are
240+
defined in the same Django app that runs beat** — i.e. beat imports
241+
every task it dispatches. At startup it disables any enabled
242+
``PeriodicTask`` whose dotted task name is not registered with the
243+
running Celery app, which prevents ``NotRegistered`` errors that
244+
otherwise persist indefinitely after a task is removed from the
245+
codebase. Disabled rows are not deleted; they can be re-enabled from
246+
the admin if the task name becomes registered again.
247+
248+
- ``django_celery_beat.schedulers:DatabaseScheduler`` (alias
249+
``django``): **the default — use it for mixed deployments** where
250+
beat dispatches tasks to workers running different codebases (and
251+
therefore the beat process does not import every task it routes).
252+
In that setup, an unknown name is expected, not an error, so the
253+
strict variant would incorrectly disable valid tasks.
254+
255+
231256

232257
Working with django-celery-results
233258
-----------------------------------

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def reqs(*f):
126126
entry_points={
127127
'celery.beat_schedulers': [
128128
'django = django_celery_beat.schedulers:DatabaseScheduler',
129+
'django_strict = django_celery_beat.schedulers:StrictDatabaseScheduler',
129130
],
130131
},
131132
include_package_data=True,

t/unit/test_schedulers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,37 @@ def test_scheduler_valid_hours(self):
14081408
assert 0 <= hour_value <= 23
14091409

14101410

1411+
@pytest.mark.django_db
1412+
class test_StrictDatabaseScheduler(SchedulerCase):
1413+
Scheduler = schedulers.StrictDatabaseScheduler
1414+
1415+
@pytest.fixture(autouse=True)
1416+
def setup_scheduler(self, app):
1417+
self.app = app
1418+
self.app.conf.beat_schedule = {}
1419+
1420+
self.known = self.create_model_interval(
1421+
schedule(timedelta(seconds=10)),
1422+
task='celery.backend_cleanup',
1423+
)
1424+
self.known.save()
1425+
1426+
self.unknown = self.create_model_interval(
1427+
schedule(timedelta(seconds=10)),
1428+
task='nonexistent.task.that.was.removed',
1429+
)
1430+
self.unknown.save()
1431+
1432+
def test_unregistered_task_is_disabled(self):
1433+
self.Scheduler(app=self.app)
1434+
1435+
self.unknown.refresh_from_db()
1436+
assert self.unknown.enabled is False
1437+
1438+
self.known.refresh_from_db()
1439+
assert self.known.enabled is True
1440+
1441+
14111442
@pytest.mark.django_db
14121443
class test_models(SchedulerCase):
14131444

0 commit comments

Comments
 (0)