File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff 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 ()
Original file line number Diff line number Diff line change @@ -228,6 +228,31 @@ with only one command (recommended for **development environment only**):
2282283. 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
232257Working with django-celery-results
233258-----------------------------------
Original file line number Diff line number Diff 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 ,
Original file line number Diff line number Diff 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
14121443class test_models (SchedulerCase ):
14131444
You can’t perform that action at this time.
0 commit comments