Dispatcharr/tests/test_celery_plugin_discovery.py

85 lines
3.6 KiB
Python

"""
Regression tests for the worker_process_init hook in `dispatcharr/celery.py`
that eagerly discovers plugins so their @shared_task definitions
register with the worker before beat starts firing.
Without this hook, plugins shipping module-level @shared_task code
(e.g. cron-scheduled background jobs) silently miss the first beat
tick after every worker restart — the worker logs
`Received unregistered task` and beat advances `last_run_at` anyway,
hiding the failure. See #1244.
"""
import weakref
from unittest.mock import MagicMock, patch
from django.test import SimpleTestCase
class WorkerProcessInitPluginDiscoveryTests(SimpleTestCase):
def test_invokes_discover_plugins_with_sync_db_false(self):
"""The handler must call PluginManager.discover_plugins(sync_db=False).
sync_db=False is intentional: discovery on every worker boot must
not touch the DB schema, just import plugin modules so their
@shared_task decorators run."""
from dispatcharr.celery import init_worker_process
mock_pm = MagicMock()
with patch(
"apps.plugins.loader.PluginManager.get", return_value=mock_pm
) as mock_get:
with patch("django.db.connections.close_all"):
init_worker_process()
mock_get.assert_called_once()
mock_pm.discover_plugins.assert_called_once_with(sync_db=False)
def test_closes_inherited_db_connections_before_discovery(self):
from dispatcharr.celery import init_worker_process
with patch("django.db.connections.close_all") as mock_close_all:
with patch("apps.plugins.loader.PluginManager.get"):
init_worker_process()
mock_close_all.assert_called_once()
def test_swallows_plugin_loader_errors(self):
"""If the plugin loader explodes, the worker must still come up —
the handler must not propagate exceptions."""
from dispatcharr.celery import init_worker_process
with patch("django.db.connections.close_all"):
with patch(
"apps.plugins.loader.PluginManager.get",
side_effect=RuntimeError("plugin loader exploded"),
):
# Must NOT raise.
init_worker_process()
def test_swallows_discover_plugins_errors(self):
"""Failure inside discover_plugins itself (e.g. one plugin's
plugin.py has an import error) must also be swallowed — one bad
plugin shouldn't keep the worker from coming up."""
from dispatcharr.celery import init_worker_process
mock_pm = MagicMock()
mock_pm.discover_plugins.side_effect = ImportError("bad plugin")
with patch("django.db.connections.close_all"):
with patch(
"apps.plugins.loader.PluginManager.get", return_value=mock_pm
):
# Must NOT raise.
init_worker_process()
def test_handler_is_connected_to_worker_process_init(self):
"""The connect decorator must have wired the handler into the
worker_process_init signal so Celery actually fires it after fork."""
from celery.signals import worker_process_init
from dispatcharr.celery import init_worker_process
receivers = [r for _, r in worker_process_init.receivers]
callables = [r() if isinstance(r, weakref.ref) else r for r in receivers]
assert init_worker_process in receivers or \
any(getattr(c, "__wrapped__", c) is init_worker_process for c in callables), (
"init_worker_process was not connected to "
"Celery's worker_process_init signal"
)