Bypass redis for management commands.

This commit is contained in:
SergeantPanda 2025-03-22 12:23:54 -05:00
parent 071efaf017
commit 3c3961bb3a
5 changed files with 124 additions and 10 deletions

View file

@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
from core.models import StreamProfile from core.models import StreamProfile
from django.conf import settings from django.conf import settings
from core.models import StreamProfile, CoreSettings from core.models import StreamProfile, CoreSettings
from core.utils import redis_client from core.utils import redis_client, execute_redis_command
import logging import logging
import uuid import uuid
from datetime import datetime 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: # If you have an M3UAccount model in apps.m3u, you can still import it:
from apps.m3u.models import M3UAccount 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): class ChannelGroup(models.Model):
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True)

34
core/command_utils.py Normal file
View file

@ -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

View file

@ -10,6 +10,23 @@ from redis.exceptions import ConnectionError, TimeoutError
logger = logging.getLogger(__name__) 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: class RedisPubSubManager:
""" """
A robust Redis PubSub manager that handles disconnections and reconnections. A robust Redis PubSub manager that handles disconnections and reconnections.
@ -23,9 +40,7 @@ class RedisPubSubManager:
redis_client: An existing Redis client to use redis_client: An existing Redis client to use
auto_reconnect: Whether to automatically reconnect on failure auto_reconnect: Whether to automatically reconnect on failure
""" """
from .utils import get_redis_client self.redis_client = redis_client
self.redis_client = redis_client or get_redis_client()
self.pubsub = None self.pubsub = None
self.subscriptions = set() self.subscriptions = set()
self.pattern_subscriptions = set() self.pattern_subscriptions = set()
@ -34,6 +49,7 @@ class RedisPubSubManager:
self.lock = threading.RLock() self.lock = threading.RLock()
self.message_handlers = {} # Map of channels to handler functions self.message_handlers = {} # Map of channels to handler functions
self.message_thread = None self.message_thread = None
self.is_dummy = redis_client is None
def subscribe(self, channel, handler=None): def subscribe(self, channel, handler=None):
""" """
@ -43,6 +59,9 @@ class RedisPubSubManager:
channel: The channel to subscribe to channel: The channel to subscribe to
handler: Optional function to call when messages are received handler: Optional function to call when messages are received
""" """
if self.is_dummy:
return
with self.lock: with self.lock:
self.subscriptions.add(channel) self.subscriptions.add(channel)
if handler: if handler:
@ -60,6 +79,9 @@ class RedisPubSubManager:
pattern: The pattern to subscribe to pattern: The pattern to subscribe to
handler: Optional function to call when messages are received handler: Optional function to call when messages are received
""" """
if self.is_dummy:
return
with self.lock: with self.lock:
self.pattern_subscriptions.add(pattern) self.pattern_subscriptions.add(pattern)
if handler: if handler:
@ -80,6 +102,9 @@ class RedisPubSubManager:
Returns: Returns:
Number of clients that received the message Number of clients that received the message
""" """
if self.is_dummy:
return 0
try: try:
if not isinstance(message, str): if not isinstance(message, str):
message = json.dumps(message) message = json.dumps(message)
@ -92,6 +117,10 @@ class RedisPubSubManager:
""" """
Start listening for messages in a background thread. 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: if not self.message_thread:
self._connect() self._connect()
self.message_thread = threading.Thread( self.message_thread = threading.Thread(
@ -106,6 +135,9 @@ class RedisPubSubManager:
""" """
Stop listening and clean up resources. Stop listening and clean up resources.
""" """
if self.is_dummy:
return
self.running = False self.running = False
if self.pubsub: if self.pubsub:
try: try:
@ -118,6 +150,10 @@ class RedisPubSubManager:
""" """
Establish a new PubSub connection and subscribe to all channels. Establish a new PubSub connection and subscribe to all channels.
""" """
if self.is_dummy:
self.pubsub = DummyPubSub()
return
with self.lock: with self.lock:
# Close any existing connection # Close any existing connection
if self.pubsub: if self.pubsub:
@ -144,6 +180,9 @@ class RedisPubSubManager:
""" """
Background thread that listens for messages and handles reconnections. Background thread that listens for messages and handles reconnections.
""" """
if self.is_dummy:
return
consecutive_errors = 0 consecutive_errors = 0
while self.running: while self.running:
@ -218,6 +257,11 @@ def get_pubsub_manager(redis_client=None):
if pubsub_manager is None: if pubsub_manager is None:
pubsub_manager = RedisPubSubManager(redis_client) 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 return pubsub_manager

View file

@ -8,8 +8,16 @@ from redis.exceptions import ConnectionError, TimeoutError
logger = logging.getLogger(__name__) 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): def get_redis_client(max_retries=5, retry_interval=1):
"""Get Redis client with connection validation and retry logic""" """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 retry_count = 0
while retry_count < max_retries: while retry_count < max_retries:
try: 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): def get_redis_pubsub_client(max_retries=5, retry_interval=1):
"""Get Redis client optimized for PubSub operations""" """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 retry_count = 0
while retry_count < max_retries: while retry_count < max_retries:
try: try:
@ -133,9 +146,20 @@ def execute_redis_command(redis_client, command_func, default_return=None):
return default_return return default_return
# Initialize the global clients with retry logic # Initialize the global clients with retry logic
redis_client = get_redis_client() # Skip Redis initialization if running as a management command
redis_pubsub_client = get_redis_pubsub_client() 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 # Import and initialize the PubSub manager
from .redis_pubsub import get_pubsub_manager # Skip if running as management command or if Redis client is None
pubsub_manager = get_pubsub_manager(redis_client) 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

View file

@ -3,7 +3,8 @@ FROM python:3.13-slim AS builder
ENV PATH="/dispatcharrpy/bin:$PATH" \ ENV PATH="/dispatcharrpy/bin:$PATH" \
VIRTUAL_ENV=/dispatcharrpy \ VIRTUAL_ENV=/dispatcharrpy \
DJANGO_SETTINGS_MODULE=dispatcharr.settings \ DJANGO_SETTINGS_MODULE=dispatcharr.settings \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1 \
DISPATCHARR_BUILD=1
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \