From f4dce6db01e063d050047754de8752d9597a36aa Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Wed, 13 May 2026 17:30:09 -0500 Subject: [PATCH] fix: Ensure plugin discovery runs in all non-Celery processes to apply monkey-patches in uWSGI workers --- CHANGELOG.md | 1 + apps/plugins/apps.py | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58243cdf..c3fc6d31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/apps/plugins/apps.py b/apps/plugins/apps.py index 72b0b068..e2b647e1 100644 --- a/apps/plugins/apps.py +++ b/apps/plugins/apps.py @@ -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