mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-27 03:54:56 +00:00
Fix Redis flush and wait_for_redis in modular mode
- Move modular Redis wait from uWSGI exec-pre to entrypoint (exec-pre runs under 'su -' which strips Docker env vars, so DISPATCHARR_ENV and REDIS_HOST were never available) - Selective flush in modular mode: clears stale app state (stream locks, proxy metadata) while preserving Celery broker/result keys - AIO mode unchanged: full flushdb via uWSGI exec-pre - Update unit tests for both flush paths
This commit is contained in:
parent
6ff81e6287
commit
4f41c287ac
4 changed files with 66 additions and 24 deletions
|
|
@ -175,23 +175,17 @@ else
|
|||
check_external_postgres_version || exit 1
|
||||
fi
|
||||
|
||||
# Wait for Redis to be ready (modular mode uses external Redis)
|
||||
# Wait for Redis to be ready and flush stale state.
|
||||
# In modular mode Redis is external — call wait_for_redis.py here
|
||||
# because uWSGI's exec-pre runs under 'su -' which strips env vars
|
||||
# (DISPATCHARR_ENV, REDIS_HOST, etc.).
|
||||
# In AIO mode Redis is started by uWSGI (attach-daemon), so the
|
||||
# exec-pre in uwsgi.ini handles the wait + flush there instead.
|
||||
if [[ "$DISPATCHARR_ENV" == "modular" ]]; then
|
||||
echo "🔗 Modular mode: Using external Redis at ${REDIS_HOST}:${REDIS_PORT}"
|
||||
echo_with_timestamp "Waiting for external Redis to be ready..."
|
||||
until python3 -c "
|
||||
import socket, sys
|
||||
try:
|
||||
s = socket.create_connection(('${REDIS_HOST}', ${REDIS_PORT}), timeout=2)
|
||||
s.close()
|
||||
sys.exit(0)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
" 2>/dev/null; do
|
||||
echo_with_timestamp "Waiting for Redis at ${REDIS_HOST}:${REDIS_PORT}..."
|
||||
sleep 1
|
||||
done
|
||||
echo "✅ External Redis is ready"
|
||||
echo_with_timestamp "Waiting for Redis to be ready..."
|
||||
python3 /app/scripts/wait_for_redis.py
|
||||
echo "✅ Redis is ready"
|
||||
fi
|
||||
|
||||
# Ensure database encoding is UTF8 (handles both internal and external databases)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
; exec-pre = touch /data/logs/uwsgi.log
|
||||
; exec-pre = chmod 666 /data/logs/uwsgi.log
|
||||
|
||||
; First run Redis availability check script once
|
||||
exec-pre = python /app/scripts/wait_for_redis.py
|
||||
; Redis wait + flush is handled by the entrypoint in modular mode
|
||||
; (uWSGI exec-pre runs under 'su -' which strips Docker env vars)
|
||||
|
||||
; Start Daphne for WebSocket support (required for real-time features)
|
||||
; Redis and Celery run in separate containers in modular mode
|
||||
|
|
|
|||
|
|
@ -12,6 +12,28 @@ import logging
|
|||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Key prefixes used by Celery's broker (Kombu) and result backend.
|
||||
# These must be preserved in modular mode where Celery runs independently.
|
||||
_CELERY_KEY_PREFIXES = ('celery', '_kombu', 'unacked')
|
||||
|
||||
|
||||
def _flush_non_celery_keys(client):
|
||||
"""Delete all Redis keys except those belonging to Celery."""
|
||||
cursor = '0'
|
||||
deleted = 0
|
||||
while True:
|
||||
cursor, keys = client.scan(cursor=cursor, count=500)
|
||||
to_delete = [
|
||||
k for k in keys
|
||||
if not k.decode('utf-8', errors='replace').startswith(_CELERY_KEY_PREFIXES)
|
||||
]
|
||||
if to_delete:
|
||||
deleted += client.delete(*to_delete)
|
||||
if cursor == 0:
|
||||
break
|
||||
logger.info(f"Modular mode: selectively cleared {deleted} non-Celery Redis key(s)")
|
||||
|
||||
|
||||
def wait_for_redis(host='localhost', port=6379, db=0, password='', username='', max_retries=30, retry_interval=2):
|
||||
"""Wait for Redis to become available"""
|
||||
redis_client = None
|
||||
|
|
@ -31,6 +53,15 @@ def wait_for_redis(host='localhost', port=6379, db=0, password='', username='',
|
|||
socket_connect_timeout=2
|
||||
)
|
||||
redis_client.ping()
|
||||
# Clear stale state on startup. In AIO mode, every service restarts
|
||||
# together so a full flush is safe. In modular mode, Celery has its
|
||||
# own lifecycle — preserve its broker/result keys and only wipe
|
||||
# application state (stream locks, proxy metadata, etc.).
|
||||
if os.environ.get('DISPATCHARR_ENV') == 'modular':
|
||||
_flush_non_celery_keys(redis_client)
|
||||
else:
|
||||
redis_client.flushdb()
|
||||
logger.info(f"Flushed Redis database")
|
||||
logger.info(f"✅ Redis at {host}:{port}/{db} is now available!")
|
||||
return True
|
||||
except (redis.exceptions.ConnectionError, redis.exceptions.TimeoutError) as e:
|
||||
|
|
|
|||
|
|
@ -21,23 +21,40 @@ class WaitForRedisTests(SimpleTestCase):
|
|||
"""
|
||||
Tests for scripts/wait_for_redis.py.
|
||||
|
||||
The critical regression test is test_successful_connection_does_not_call_flushdb
|
||||
which ensures the removed flushdb() call is never re-added.
|
||||
Verifies flush behaviour: full flushdb in AIO mode, selective
|
||||
(non-Celery) key deletion in modular mode.
|
||||
"""
|
||||
|
||||
@patch('wait_for_redis.redis.Redis')
|
||||
def test_successful_connection_does_not_call_flushdb(self, mock_redis_cls):
|
||||
"""After connecting successfully, flushdb must NOT be called."""
|
||||
def test_aio_mode_calls_flushdb(self, mock_redis_cls):
|
||||
"""In AIO mode (default), flushdb is called after successful ping."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.ping.return_value = True
|
||||
mock_redis_cls.return_value = mock_client
|
||||
|
||||
wait_for_redis = _import_wait_for_redis()
|
||||
result = wait_for_redis(max_retries=1, retry_interval=0)
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
os.environ.pop('DISPATCHARR_ENV', None)
|
||||
wait_for_redis = _import_wait_for_redis()
|
||||
result = wait_for_redis(max_retries=1, retry_interval=0)
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_client.flushdb.assert_called_once()
|
||||
|
||||
@patch('wait_for_redis._flush_non_celery_keys')
|
||||
@patch('wait_for_redis.redis.Redis')
|
||||
def test_modular_mode_does_not_call_flushdb(self, mock_redis_cls, mock_selective):
|
||||
"""In modular mode, flushdb must NOT be called — selective flush instead."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.ping.return_value = True
|
||||
mock_redis_cls.return_value = mock_client
|
||||
|
||||
with patch.dict(os.environ, {'DISPATCHARR_ENV': 'modular'}):
|
||||
wait_for_redis = _import_wait_for_redis()
|
||||
result = wait_for_redis(max_retries=1, retry_interval=0)
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_client.ping.assert_called_once()
|
||||
mock_client.flushdb.assert_not_called()
|
||||
mock_selective.assert_called_once_with(mock_client)
|
||||
|
||||
@patch('wait_for_redis.redis.Redis')
|
||||
def test_retries_on_connection_error(self, mock_redis_cls):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue