Enhancement: Refactored app initialization to prevent redundant execution across multiple worker processes. Created dispatcharr.app_initialization utility module with should_skip_initialization() function that prevents custom initialization tasks (backup scheduler sync, developer notifications sync) from running during management commands, in worker processes, or in development servers. This significantly reduces startup overhead in multi-worker deployments (e.g., uWSGI with 10 workers now syncs the scheduler once instead of 10 times). Applied to both CoreConfig and BackupsConfig apps.

This commit is contained in:
SergeantPanda 2026-02-05 13:48:01 -06:00
parent da5fbb1f5c
commit de81dc6c80
4 changed files with 67 additions and 49 deletions

View file

@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- XtreamCodes Authentication Optimization: Reduced API calls during XC refresh by 50% by eliminating redundant authentication step. This should help reduce rate-limiting errors.
- App initialization efficiency: Refactored app initialization to prevent redundant execution across multiple worker processes. Created `dispatcharr.app_initialization` utility module with `should_skip_initialization()` function that prevents custom initialization tasks (backup scheduler sync, developer notifications sync) from running during management commands, in worker processes, or in development servers. This significantly reduces startup overhead in multi-worker deployments (e.g., uWSGI with 10 workers now syncs the scheduler once instead of 10 times). Applied to both `CoreConfig` and `BackupsConfig` apps.
- M3U/EPG Network Access Defaults: Updated default network access settings for M3U and EPG endpoints to only allow local/private networks by default (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1/128, fc00::/7, fe80::/10). This improves security by preventing public internet access to these endpoints unless explicitly configured. Other endpoints (Streams, XC API, UI) remain open by default.
- Modular deployments: Bumped modular Postgres image to 17 and added compatibility checks (PostgreSQL version and UTF-8 database encoding) when using external databases to prevent migration/encoding issues.
- Stream Identity Stability: Added `stream_id` (provider stream identifier) and `stream_chno` (provider channel number) fields to Stream model. For XC accounts, the stream hash now uses the stable `stream_id` instead of the URL when hashing, ensuring XC streams maintain their identity and channel associations even when account credentials or server URLs change. Supports both XC `num` and M3U `tvg-chno`/`channel-number` attributes.

View file

@ -1,24 +1,10 @@
import logging
import sys
import os
from django.apps import AppConfig
import psutil
logger = logging.getLogger(__name__)
def _is_worker_process():
"""Check if this process is a worker spawned by uwsgi/gunicorn."""
try:
parent = psutil.Process(os.getppid())
parent_name = parent.name()
return parent_name in ['uwsgi', 'gunicorn']
except Exception:
# If we can't determine, assume it's not a worker (safe default)
return False
class BackupsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.backups"
@ -26,25 +12,17 @@ class BackupsConfig(AppConfig):
def ready(self):
"""Initialize backup scheduler on app startup."""
# Skip management commands and celery
skip_commands = ['celery', 'beat', 'migrate', 'makemigrations', 'shell', 'dbshell', 'collectstatic', 'loaddata']
if any(cmd in sys.argv for cmd in skip_commands):
from dispatcharr.app_initialization import should_skip_initialization
# Skip if this is a management command, worker process, or dev server
if should_skip_initialization():
return
# Skip daphne dev server
if 'daphne' in sys.argv[0] if sys.argv else False:
return
# Skip if this is a worker process spawned by uwsgi/gunicorn
if _is_worker_process():
return
# Proceed with syncing the backup scheduler
logger.debug("Syncing backup scheduler on app startup")
self._sync_backup_scheduler()
def _sync_backup_scheduler(self):
"""Sync backup scheduler task to database."""
# Import here to avoid circular imports
from core.models import CoreSettings
from .scheduler import _sync_periodic_task, DEFAULTS
try:

View file

@ -1,6 +1,6 @@
from django.apps import AppConfig
from django.conf import settings
import os, logging
import logging
# Define TRACE level (5 is below DEBUG which is 10)
TRACE = 5
@ -15,6 +15,7 @@ def trace(self, message, *args, **kwargs):
# Add the trace method to the Logger class
logging.Logger.trace = trace
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'
@ -22,13 +23,15 @@ class CoreConfig(AppConfig):
def ready(self):
# Import signals to ensure they get registered
import core.signals
from dispatcharr.app_initialization import should_skip_initialization
# Sync developer notifications and check for version updates on startup
# Only run in the main process (not in management commands or migrations)
import sys
if 'runserver' in sys.argv or 'uwsgi' in sys.argv[0] if sys.argv else False:
self._sync_developer_notifications()
self._check_version_update()
# Only run in the main process (not in management commands, migrations, or workers)
if should_skip_initialization():
return
self._sync_developer_notifications()
self._check_version_update()
def _sync_developer_notifications(self):
"""Sync developer notifications from JSON file to database."""
@ -37,22 +40,6 @@ class CoreConfig(AppConfig):
logger = logging.getLogger(__name__)
# Check if tables exist (avoid running during migrations)
try:
with connection.cursor() as cursor:
cursor.execute(
"SELECT 1 FROM information_schema.tables WHERE table_name = 'core_systemnotification'"
)
if not cursor.fetchone():
# For SQLite
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='core_systemnotification'"
)
if not cursor.fetchone():
return
except Exception:
# If we can't check, the table might not exist yet
pass
try:
from core.developer_notifications import sync_developer_notifications

View file

@ -0,0 +1,52 @@
"""Utilities for managing app initialization across multiple processes."""
import sys
import os
import psutil
import logging
logger = logging.getLogger(__name__)
def _is_worker_process():
"""Check if this process is a worker spawned by uwsgi/gunicorn."""
try:
parent = psutil.Process(os.getppid())
parent_name = parent.name()
return parent_name in ['uwsgi', 'gunicorn']
except Exception:
# If we can't determine, assume it's not a worker (safe default)
return False
def should_skip_initialization():
"""
Determine if app initialization should be skipped in this process.
Returns True if:
- A management command is being run (migrate, celery, shell, etc.)
- The development server (daphne) is running
- This is a worker process (not the master)
This prevents redundant initialization across multiple worker processes.
"""
# Skip management commands and background services
skip_commands = [
'celery', 'beat', 'migrate', 'makemigrations', 'shell', 'dbshell',
'collectstatic', 'loaddata'
]
if any(cmd in sys.argv for cmd in skip_commands):
logger.debug(f"Skipping initialization due to command: {sys.argv}")
return True
# Skip daphne development server (single process, no need to guard)
if 'daphne' in sys.argv[0] if sys.argv else False:
logger.debug(f"Skipping initialization in daphne development server. Command: {sys.argv}")
return True
# Skip if this is a worker process spawned by uwsgi/gunicorn
if _is_worker_process():
logger.debug(f"Skipping initialization in worker process. Command: {sys.argv}")
return True
return False