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

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__)
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

View file

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

View file

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