Plugins live in /data/plugins/<slug>/plugin.py, outside INSTALLED_APPS,
so celery's autodiscover_tasks() never imports them. Any plugin using
module-level @shared_task for cron-scheduled work therefore has its
task unregistered with the worker after every worker restart, until
something else (a connect/utils.trigger_event call, etc.) lazily
imports the plugin module via PluginManager.discover_plugins.
In the meantime, beat fires the periodic task on time and the worker
rejects it:
ERROR celery.worker.consumer.consumer Received unregistered task of
type 'telegram_alerts.send_daily_report'.
PeriodicTask.last_run_at advances anyway, so the failure is silent at
the default INFO log level.
Fix: hook celery's worker_ready signal in dispatcharr/celery.py to
call PluginManager.discover_plugins(sync_db=False) on every worker
boot. Importing the plugin modules runs their @shared_task decorators,
registering the tasks before beat starts firing.
Exception handling is intentionally broad: if one plugin's plugin.py
has an import error, the worker must still come up — discovery failure
should not be a fatal error for the whole worker process.
Tests in tests/test_celery_plugin_discovery.py verify:
- the handler calls discover_plugins(sync_db=False)
- the handler swallows PluginManager.get() exceptions
- the handler swallows discover_plugins() exceptions
- the handler is actually connected to the worker_ready signal
Confirmed the test fails against current dev with the exact
'cannot import name discover_plugins_on_worker_ready' that motivates
this change, and passes with the fix.