fix: Ensure plugin discovery runs in all non-Celery processes to apply monkey-patches in uWSGI workers
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run

This commit is contained in:
SergeantPanda 2026-05-13 17:30:09 -05:00
parent eba261b6af
commit f4dce6db01
2 changed files with 17 additions and 9 deletions

View file

@ -92,6 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **DVR section count badges appeared at the far right of the screen.** The "Currently Recording", "Upcoming Recordings", and "Previously Recorded" section headers wrapped the title and badge in a `Group justify="space-between"`, which pushed the badge to the opposite edge of the full-width container. Changed to `Group gap="xs" align="center"` so each badge sits inline with its heading.
- **Plugin event dispatch aborted silently on first disabled plugin.** `trigger_event` in `apps/connect/utils.py` iterated `pm.list_plugins()` and, for disabled plugins, logged a debug message using `plugin.key` / `plugin.name` (attribute access). Because `list_plugins()` returns dicts, this raised `AttributeError` on the first disabled plugin encountered. Fixed by changing the two accesses to `plugin['key']` / `plugin['name']`. (Fixes #1231) - Thanks [@R3XCHRIS](https://github.com/R3XCHRIS)
- **Plugin periodic tasks silently missed the first beat tick after every Celery worker restart.** Plugin modules live outside `INSTALLED_APPS` so `autodiscover_tasks()` never imports them, and worker startup skips plugin discovery via `should_skip_initialization()`. Any plugin using module-level `@shared_task` had its tasks unregistered with the worker until a lazy event import warmed the module; beat fired on schedule but the worker rejected with `Received unregistered task` and advanced `last_run_at` anyway, hiding the miss. A `worker_ready` hook in `dispatcharr/celery.py` now eagerly calls `PluginManager.discover_plugins(sync_db=False)` on every worker boot so plugin tasks are registered before beat starts firing. (Fixes #1244) - Thanks [@R3XCHRIS](https://github.com/R3XCHRIS)
- **Plugin monkey-patches and module-level hooks were never applied in uWSGI workers.** Both uWSGI configs use `lazy-apps=true`, meaning each worker boots independently and never inherits state from the master. `should_skip_initialization()` correctly skipped one-shot startup tasks in workers, but also blocked `discover_plugins`, so plugin modules were never imported in any of the 4 request-serving workers. Plugins relying on patching request handling (monkey-patches, signal registrations) were silently inactive until a Connect event lazily triggered discovery in that specific worker. Discovery is now run in every process that serves requests, gated only for Celery processes (which use `worker_ready`) and management commands that don't serve requests.
### Performance

View file

@ -13,11 +13,12 @@ class PluginsConfig(AppConfig):
- Skip during common management commands that don't need discovery.
- Register post_migrate handler to sync plugin registry to DB after migrations.
- Do an in-memory discovery (no DB) so registry is available early
but only in the main uwsgi/daphne process. Celery workers and
management commands skip the eager pass: the post_migrate handler
covers the DB sync, and `connect/utils.py` lazy-discovers on first
event using the loader's cache.
- Run in-memory discovery (no DB) in every non-Celery process so plugin
modules are imported and monkey-patches apply. This includes uWSGI workers
under lazy-apps=true, which each start cold and never inherit a warmed fork.
Celery workers skip here and discover via the worker_ready signal instead.
- One-shot startup tasks (schedule setup, repo refresh) are gated by
should_skip_initialization() so they only fire in the master/main process.
"""
try:
# Allow explicit opt-out via env var
@ -47,13 +48,19 @@ class PluginsConfig(AppConfig):
_post_migrate_discover,
dispatch_uid="apps.plugins.post_migrate_discover",
)
#
from dispatcharr.app_initialization import should_skip_initialization
# Skip discovery for Celery (worker_ready signal handles it) and
# pure management commands that don't serve requests. Every other
# process - including each uWSGI worker under lazy-apps=true - runs
# discovery so plugin modules are imported and monkey-patches apply.
_no_discovery_cmds = {'celery', 'beat', 'migrate', 'dbshell', 'loaddata'}
if not any(cmd in sys.argv for cmd in _no_discovery_cmds):
from .loader import PluginManager
PluginManager.get().discover_plugins(sync_db=False)
if should_skip_initialization():
return
from .loader import PluginManager
PluginManager.get().discover_plugins(sync_db=False)
except Exception:
# Avoid breaking startup due to plugin errors
import logging