diff --git a/CHANGELOG.md b/CHANGELOG.md index f711d67c..babec5d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/apps/backups/apps.py b/apps/backups/apps.py index 6b34d933..d22ffb29 100644 --- a/apps/backups/apps.py +++ b/apps/backups/apps.py @@ -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: diff --git a/core/apps.py b/core/apps.py index 12f6d054..f2780bd1 100644 --- a/core/apps.py +++ b/core/apps.py @@ -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 diff --git a/dispatcharr/app_initialization.py b/dispatcharr/app_initialization.py new file mode 100644 index 00000000..e2fc12d6 --- /dev/null +++ b/dispatcharr/app_initialization.py @@ -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