From 3c3961bb3aa462fbc144674ba309ceb20db0a3e1 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sat, 22 Mar 2025 12:23:54 -0500 Subject: [PATCH] Bypass redis for management commands. --- apps/channels/models.py | 13 ++++++++++- core/command_utils.py | 34 +++++++++++++++++++++++++++ core/redis_pubsub.py | 52 +++++++++++++++++++++++++++++++++++++---- core/utils.py | 32 +++++++++++++++++++++---- docker/Dockerfile | 3 ++- 5 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 core/command_utils.py diff --git a/apps/channels/models.py b/apps/channels/models.py index df291e09..ec95e309 100644 --- a/apps/channels/models.py +++ b/apps/channels/models.py @@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError from core.models import StreamProfile from django.conf import settings from core.models import StreamProfile, CoreSettings -from core.utils import redis_client +from core.utils import redis_client, execute_redis_command import logging import uuid from datetime import datetime @@ -15,6 +15,17 @@ logger = logging.getLogger(__name__) # If you have an M3UAccount model in apps.m3u, you can still import it: from apps.m3u.models import M3UAccount +# Add fallback functions if Redis isn't available +def get_total_viewers(channel_id): + """Get viewer count from Redis or return 0 if Redis isn't available""" + if redis_client is None: + return 0 + + try: + return int(redis_client.get(f"channel:{channel_id}:viewers") or 0) + except Exception: + return 0 + class ChannelGroup(models.Model): name = models.CharField(max_length=100, unique=True) diff --git a/core/command_utils.py b/core/command_utils.py new file mode 100644 index 00000000..f6adfd88 --- /dev/null +++ b/core/command_utils.py @@ -0,0 +1,34 @@ +import sys +import os + +def is_management_command(excluded_commands=None): + """ + Detect if we're running a Django management command like migrate, collectstatic, etc. + + Args: + excluded_commands: List of commands that should still use Redis (e.g. runserver) + + Returns: + bool: True if we're running a management command + """ + # First check if we're in build mode + if os.environ.get("DISPATCHARR_BUILD") == "1": + return True + + if excluded_commands is None: + excluded_commands = ['runserver', 'runworker', 'daphne'] + + # Check if we're running via manage.py + if not ('manage.py' in sys.argv[0]): + return False + + # Check if we have a command argument + if len(sys.argv) > 1: + command = sys.argv[1] + # Return False if command is in excluded list - these commands DO need Redis + if command in excluded_commands: + return False + # Otherwise it's a command that should work without Redis + return True + + return False diff --git a/core/redis_pubsub.py b/core/redis_pubsub.py index 5fb57334..5d0032b0 100644 --- a/core/redis_pubsub.py +++ b/core/redis_pubsub.py @@ -10,6 +10,23 @@ from redis.exceptions import ConnectionError, TimeoutError logger = logging.getLogger(__name__) +class DummyPubSub: + """Dummy PubSub implementation when Redis isn't available""" + def __init__(self): + pass + + def subscribe(self, *args, **kwargs): + pass + + def psubscribe(self, *args, **kwargs): + pass + + def get_message(self, *args, **kwargs): + return None + + def close(self): + pass + class RedisPubSubManager: """ A robust Redis PubSub manager that handles disconnections and reconnections. @@ -23,9 +40,7 @@ class RedisPubSubManager: redis_client: An existing Redis client to use auto_reconnect: Whether to automatically reconnect on failure """ - from .utils import get_redis_client - - self.redis_client = redis_client or get_redis_client() + self.redis_client = redis_client self.pubsub = None self.subscriptions = set() self.pattern_subscriptions = set() @@ -34,6 +49,7 @@ class RedisPubSubManager: self.lock = threading.RLock() self.message_handlers = {} # Map of channels to handler functions self.message_thread = None + self.is_dummy = redis_client is None def subscribe(self, channel, handler=None): """ @@ -43,6 +59,9 @@ class RedisPubSubManager: channel: The channel to subscribe to handler: Optional function to call when messages are received """ + if self.is_dummy: + return + with self.lock: self.subscriptions.add(channel) if handler: @@ -60,6 +79,9 @@ class RedisPubSubManager: pattern: The pattern to subscribe to handler: Optional function to call when messages are received """ + if self.is_dummy: + return + with self.lock: self.pattern_subscriptions.add(pattern) if handler: @@ -80,6 +102,9 @@ class RedisPubSubManager: Returns: Number of clients that received the message """ + if self.is_dummy: + return 0 + try: if not isinstance(message, str): message = json.dumps(message) @@ -92,6 +117,10 @@ class RedisPubSubManager: """ Start listening for messages in a background thread. """ + if self.is_dummy: + logger.debug("Running with dummy Redis client - not starting listener") + return + if not self.message_thread: self._connect() self.message_thread = threading.Thread( @@ -106,6 +135,9 @@ class RedisPubSubManager: """ Stop listening and clean up resources. """ + if self.is_dummy: + return + self.running = False if self.pubsub: try: @@ -118,6 +150,10 @@ class RedisPubSubManager: """ Establish a new PubSub connection and subscribe to all channels. """ + if self.is_dummy: + self.pubsub = DummyPubSub() + return + with self.lock: # Close any existing connection if self.pubsub: @@ -144,6 +180,9 @@ class RedisPubSubManager: """ Background thread that listens for messages and handles reconnections. """ + if self.is_dummy: + return + consecutive_errors = 0 while self.running: @@ -218,6 +257,11 @@ def get_pubsub_manager(redis_client=None): if pubsub_manager is None: pubsub_manager = RedisPubSubManager(redis_client) - pubsub_manager.start_listening() + # Only start if redis_client is not None + if redis_client is not None: + try: + pubsub_manager.start_listening() + except Exception as e: + logger.error(f"Failed to start PubSub listener: {e}") return pubsub_manager diff --git a/core/utils.py b/core/utils.py index 4e472ef2..073c2169 100644 --- a/core/utils.py +++ b/core/utils.py @@ -8,8 +8,16 @@ from redis.exceptions import ConnectionError, TimeoutError logger = logging.getLogger(__name__) +# Import the command detector +from .command_utils import is_management_command + def get_redis_client(max_retries=5, retry_interval=1): """Get Redis client with connection validation and retry logic""" + # Skip Redis connection for management commands like collectstatic + if is_management_command(): + logger.info("Running as management command - skipping Redis initialization") + return None + retry_count = 0 while retry_count < max_retries: try: @@ -59,6 +67,11 @@ def get_redis_client(max_retries=5, retry_interval=1): def get_redis_pubsub_client(max_retries=5, retry_interval=1): """Get Redis client optimized for PubSub operations""" + # Skip Redis connection for management commands like collectstatic + if is_management_command(): + logger.info("Running as management command - skipping Redis PubSub initialization") + return None + retry_count = 0 while retry_count < max_retries: try: @@ -133,9 +146,20 @@ def execute_redis_command(redis_client, command_func, default_return=None): return default_return # Initialize the global clients with retry logic -redis_client = get_redis_client() -redis_pubsub_client = get_redis_pubsub_client() +# Skip Redis initialization if running as a management command +if is_management_command(): + redis_client = None + redis_pubsub_client = None + logger.info("Running as management command - Redis clients set to None") +else: + redis_client = get_redis_client() + redis_pubsub_client = get_redis_pubsub_client() # Import and initialize the PubSub manager -from .redis_pubsub import get_pubsub_manager -pubsub_manager = get_pubsub_manager(redis_client) \ No newline at end of file +# Skip if running as management command or if Redis client is None +if not is_management_command() and redis_client is not None: + from .redis_pubsub import get_pubsub_manager + pubsub_manager = get_pubsub_manager(redis_client) +else: + logger.info("PubSub manager not initialized (running as management command or Redis not available)") + pubsub_manager = None \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 54aa75e5..f6984f8b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,7 +3,8 @@ FROM python:3.13-slim AS builder ENV PATH="/dispatcharrpy/bin:$PATH" \ VIRTUAL_ENV=/dispatcharrpy \ DJANGO_SETTINGS_MODULE=dispatcharr.settings \ - PYTHONUNBUFFERED=1 + PYTHONUNBUFFERED=1 \ + DISPATCHARR_BUILD=1 RUN apt-get update && \ apt-get install -y --no-install-recommends \