fix: Update plugin discovery comments and improve weakref handling in tests. Update changelog.

This commit is contained in:
SergeantPanda 2026-05-13 17:02:34 -05:00
parent f38d3a4f17
commit e61539fc80
3 changed files with 7 additions and 13 deletions

View file

@ -91,6 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Recurring rule edit modal showed a blank Channel field.** The `RecurringRuleModal` read channel data from a Zustand store slice (`channels`) that nothing populates - the channels page and all other consumers switched to on-demand summary fetches in a prior release. Opening the edit modal from the DVR page always produced an empty Channel select. The modal now fetches channels via `API.getChannelsSummary()` when it opens, matching the lightweight approach used by the one-time recording form and other modals.
- **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)
### Performance

View file

@ -39,17 +39,9 @@ app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
# Plugins live in /data/plugins/<slug>/plugin.py — outside INSTALLED_APPS,
# so `app.autodiscover_tasks()` never imports them. Any plugin using
# module-level `@shared_task` for cron work would therefore have its task
# go unregistered on the worker side after every restart, until something
# else (a `trigger_event` call, etc.) lazily imported the plugin module.
# Beat would fire the periodic task on time and the worker would reject it
# with `Received unregistered task`. (#1244)
#
# Eagerly call `PluginManager.discover_plugins` on worker startup so every
# plugin's `@shared_task` is registered with this worker's Celery app
# before beat starts firing.
# Plugins live outside INSTALLED_APPS, so autodiscover_tasks() never imports
# them. Without an eager import, workers reject plugin @shared_tasks with
# "Received unregistered task" until a lazy event import warms the module.
@worker_ready.connect(weak=False)
def discover_plugins_on_worker_ready(**_kwargs):
try:

View file

@ -9,6 +9,7 @@ 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
@ -67,8 +68,8 @@ class WorkerReadyPluginDiscoveryTests(SimpleTestCase):
# The handler is registered with weak=False so the function appears
# directly (not as a dead weakref).
receivers = [r for _, r in worker_ready.receivers]
# Handle both weakref-wrapped and direct receiver shapes.
callables = [r() if hasattr(r, "__call__") and not callable(getattr(r, "__name__", None)) else r for r in receivers]
# Dereference weakrefs; pass direct references through as-is.
callables = [r() if isinstance(r, weakref.ref) else r for r in receivers]
assert discover_plugins_on_worker_ready in receivers or \
any(getattr(c, "__wrapped__", c) is discover_plugins_on_worker_ready for c in callables), (
"discover_plugins_on_worker_ready was not connected to "