mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
Bypass redis for management commands.
This commit is contained in:
parent
071efaf017
commit
3c3961bb3a
5 changed files with 124 additions and 10 deletions
|
|
@ -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
34
core/command_utils.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 \
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue