diff --git a/backend/open_webui/test/util/test_redis.py b/backend/open_webui/test/util/test_redis.py index 9fa3c90701..8c393ce9d9 100644 --- a/backend/open_webui/test/util/test_redis.py +++ b/backend/open_webui/test/util/test_redis.py @@ -6,8 +6,9 @@ from open_webui.utils.redis import ( parse_redis_service_url, get_redis_connection, get_sentinels_from_env, - MAX_RETRY_COUNT + MAX_RETRY_COUNT, ) +import inspect class TestSentinelRedisProxy: @@ -55,7 +56,7 @@ class TestSentinelRedisProxy: result = get_sentinels_from_env(None, "26379") assert result == [] - @patch('redis.sentinel.Sentinel') + @patch("redis.sentinel.Sentinel") def test_sentinel_redis_proxy_sync_success(self, mock_sentinel_class): """Test successful sync operation with SentinelRedisProxy""" mock_sentinel = Mock() @@ -73,7 +74,7 @@ class TestSentinelRedisProxy: mock_sentinel.master_for.assert_called_with("mymaster") mock_master.get.assert_called_with("test_key") - @patch('redis.sentinel.Sentinel') + @patch("redis.sentinel.Sentinel") @pytest.mark.asyncio async def test_sentinel_redis_proxy_async_success(self, mock_sentinel_class): """Test successful async operation with SentinelRedisProxy""" @@ -92,7 +93,7 @@ class TestSentinelRedisProxy: mock_sentinel.master_for.assert_called_with("mymaster") mock_master.get.assert_called_with("test_key") - @patch('redis.sentinel.Sentinel') + @patch("redis.sentinel.Sentinel") def test_sentinel_redis_proxy_failover_retry(self, mock_sentinel_class): """Test retry mechanism during failover""" mock_sentinel = Mock() @@ -101,7 +102,7 @@ class TestSentinelRedisProxy: # First call fails, second succeeds mock_master.get.side_effect = [ redis.exceptions.ConnectionError("Master down"), - "test_value" + "test_value", ] mock_sentinel.master_for.return_value = mock_master @@ -113,7 +114,7 @@ class TestSentinelRedisProxy: assert result == "test_value" assert mock_master.get.call_count == 2 - @patch('redis.sentinel.Sentinel') + @patch("redis.sentinel.Sentinel") def test_sentinel_redis_proxy_max_retries_exceeded(self, mock_sentinel_class): """Test failure after max retries exceeded""" mock_sentinel = Mock() @@ -132,7 +133,7 @@ class TestSentinelRedisProxy: assert mock_master.get.call_count == MAX_RETRY_COUNT - @patch('redis.sentinel.Sentinel') + @patch("redis.sentinel.Sentinel") def test_sentinel_redis_proxy_readonly_error_retry(self, mock_sentinel_class): """Test retry on ReadOnlyError""" mock_sentinel = Mock() @@ -141,7 +142,7 @@ class TestSentinelRedisProxy: # First call gets ReadOnlyError (old master), second succeeds (new master) mock_master.get.side_effect = [ redis.exceptions.ReadOnlyError("Read only"), - "test_value" + "test_value", ] mock_sentinel.master_for.return_value = mock_master @@ -153,7 +154,7 @@ class TestSentinelRedisProxy: assert result == "test_value" assert mock_master.get.call_count == 2 - @patch('redis.sentinel.Sentinel') + @patch("redis.sentinel.Sentinel") def test_sentinel_redis_proxy_factory_methods(self, mock_sentinel_class): """Test factory methods are passed through directly""" mock_sentinel = Mock() @@ -171,9 +172,11 @@ class TestSentinelRedisProxy: assert result == mock_pipeline mock_master.pipeline.assert_called_once() - @patch('redis.sentinel.Sentinel') - @patch('redis.from_url') - def test_get_redis_connection_with_sentinel(self, mock_from_url, mock_sentinel_class): + @patch("redis.sentinel.Sentinel") + @patch("redis.from_url") + def test_get_redis_connection_with_sentinel( + self, mock_from_url, mock_sentinel_class + ): """Test getting Redis connection with Sentinel""" mock_sentinel = Mock() mock_sentinel_class.return_value = mock_sentinel @@ -182,16 +185,14 @@ class TestSentinelRedisProxy: redis_url = "redis://user:pass@mymaster:6379/0" result = get_redis_connection( - redis_url=redis_url, - redis_sentinels=sentinels, - async_mode=False + redis_url=redis_url, redis_sentinels=sentinels, async_mode=False ) assert isinstance(result, SentinelRedisProxy) mock_sentinel_class.assert_called_once() mock_from_url.assert_not_called() - @patch('redis.Redis.from_url') + @patch("redis.Redis.from_url") def test_get_redis_connection_without_sentinel(self, mock_from_url): """Test getting Redis connection without Sentinel""" mock_redis = Mock() @@ -200,15 +201,13 @@ class TestSentinelRedisProxy: redis_url = "redis://localhost:6379/0" result = get_redis_connection( - redis_url=redis_url, - redis_sentinels=None, - async_mode=False + redis_url=redis_url, redis_sentinels=None, async_mode=False ) assert result == mock_redis mock_from_url.assert_called_once_with(redis_url, decode_responses=True) - @patch('redis.asyncio.from_url') + @patch("redis.asyncio.from_url") def test_get_redis_connection_without_sentinel_async(self, mock_from_url): """Test getting async Redis connection without Sentinel""" mock_redis = Mock() @@ -217,10 +216,578 @@ class TestSentinelRedisProxy: redis_url = "redis://localhost:6379/0" result = get_redis_connection( - redis_url=redis_url, - redis_sentinels=None, - async_mode=True + redis_url=redis_url, redis_sentinels=None, async_mode=True ) assert result == mock_redis - mock_from_url.assert_called_once_with(redis_url, decode_responses=True) \ No newline at end of file + mock_from_url.assert_called_once_with(redis_url, decode_responses=True) + + +class TestSentinelRedisProxyCommands: + """Test Redis commands through SentinelRedisProxy""" + + @patch("redis.sentinel.Sentinel") + def test_hash_commands_sync(self, mock_sentinel_class): + """Test Redis hash commands in sync mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock hash command responses + mock_master.hset.return_value = 1 + mock_master.hget.return_value = "test_value" + mock_master.hgetall.return_value = {"key1": "value1", "key2": "value2"} + mock_master.hdel.return_value = 1 + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False) + + # Test hset + hset_method = proxy.__getattr__("hset") + result = hset_method("test_hash", "field1", "value1") + assert result == 1 + mock_master.hset.assert_called_with("test_hash", "field1", "value1") + + # Test hget + hget_method = proxy.__getattr__("hget") + result = hget_method("test_hash", "field1") + assert result == "test_value" + mock_master.hget.assert_called_with("test_hash", "field1") + + # Test hgetall + hgetall_method = proxy.__getattr__("hgetall") + result = hgetall_method("test_hash") + assert result == {"key1": "value1", "key2": "value2"} + mock_master.hgetall.assert_called_with("test_hash") + + # Test hdel + hdel_method = proxy.__getattr__("hdel") + result = hdel_method("test_hash", "field1") + assert result == 1 + mock_master.hdel.assert_called_with("test_hash", "field1") + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_hash_commands_async(self, mock_sentinel_class): + """Test Redis hash commands in async mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock async hash command responses + mock_master.hset = AsyncMock(return_value=1) + mock_master.hget = AsyncMock(return_value="test_value") + mock_master.hgetall = AsyncMock( + return_value={"key1": "value1", "key2": "value2"} + ) + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Test hset + hset_method = proxy.__getattr__("hset") + result = await hset_method("test_hash", "field1", "value1") + assert result == 1 + mock_master.hset.assert_called_with("test_hash", "field1", "value1") + + # Test hget + hget_method = proxy.__getattr__("hget") + result = await hget_method("test_hash", "field1") + assert result == "test_value" + mock_master.hget.assert_called_with("test_hash", "field1") + + # Test hgetall + hgetall_method = proxy.__getattr__("hgetall") + result = await hgetall_method("test_hash") + assert result == {"key1": "value1", "key2": "value2"} + mock_master.hgetall.assert_called_with("test_hash") + + @patch("redis.sentinel.Sentinel") + def test_string_commands_sync(self, mock_sentinel_class): + """Test Redis string commands in sync mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock string command responses + mock_master.set.return_value = True + mock_master.get.return_value = "test_value" + mock_master.delete.return_value = 1 + mock_master.exists.return_value = True + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False) + + # Test set + set_method = proxy.__getattr__("set") + result = set_method("test_key", "test_value") + assert result is True + mock_master.set.assert_called_with("test_key", "test_value") + + # Test get + get_method = proxy.__getattr__("get") + result = get_method("test_key") + assert result == "test_value" + mock_master.get.assert_called_with("test_key") + + # Test delete + delete_method = proxy.__getattr__("delete") + result = delete_method("test_key") + assert result == 1 + mock_master.delete.assert_called_with("test_key") + + # Test exists + exists_method = proxy.__getattr__("exists") + result = exists_method("test_key") + assert result is True + mock_master.exists.assert_called_with("test_key") + + @patch("redis.sentinel.Sentinel") + def test_list_commands_sync(self, mock_sentinel_class): + """Test Redis list commands in sync mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock list command responses + mock_master.lpush.return_value = 1 + mock_master.rpop.return_value = "test_value" + mock_master.llen.return_value = 5 + mock_master.lrange.return_value = ["item1", "item2", "item3"] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False) + + # Test lpush + lpush_method = proxy.__getattr__("lpush") + result = lpush_method("test_list", "item1") + assert result == 1 + mock_master.lpush.assert_called_with("test_list", "item1") + + # Test rpop + rpop_method = proxy.__getattr__("rpop") + result = rpop_method("test_list") + assert result == "test_value" + mock_master.rpop.assert_called_with("test_list") + + # Test llen + llen_method = proxy.__getattr__("llen") + result = llen_method("test_list") + assert result == 5 + mock_master.llen.assert_called_with("test_list") + + # Test lrange + lrange_method = proxy.__getattr__("lrange") + result = lrange_method("test_list", 0, -1) + assert result == ["item1", "item2", "item3"] + mock_master.lrange.assert_called_with("test_list", 0, -1) + + @patch("redis.sentinel.Sentinel") + def test_pubsub_commands_sync(self, mock_sentinel_class): + """Test Redis pubsub commands in sync mode""" + mock_sentinel = Mock() + mock_master = Mock() + mock_pubsub = Mock() + + # Mock pubsub responses + mock_master.pubsub.return_value = mock_pubsub + mock_master.publish.return_value = 1 + mock_pubsub.subscribe.return_value = None + mock_pubsub.get_message.return_value = {"type": "message", "data": "test_data"} + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False) + + # Test pubsub (factory method - should pass through) + pubsub_method = proxy.__getattr__("pubsub") + result = pubsub_method() + assert result == mock_pubsub + mock_master.pubsub.assert_called_once() + + # Test publish + publish_method = proxy.__getattr__("publish") + result = publish_method("test_channel", "test_message") + assert result == 1 + mock_master.publish.assert_called_with("test_channel", "test_message") + + @patch("redis.sentinel.Sentinel") + def test_pipeline_commands_sync(self, mock_sentinel_class): + """Test Redis pipeline commands in sync mode""" + mock_sentinel = Mock() + mock_master = Mock() + mock_pipeline = Mock() + + # Mock pipeline responses + mock_master.pipeline.return_value = mock_pipeline + mock_pipeline.set.return_value = mock_pipeline + mock_pipeline.get.return_value = mock_pipeline + mock_pipeline.execute.return_value = [True, "test_value"] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False) + + # Test pipeline (factory method - should pass through) + pipeline_method = proxy.__getattr__("pipeline") + result = pipeline_method() + assert result == mock_pipeline + mock_master.pipeline.assert_called_once() + + @patch("redis.sentinel.Sentinel") + def test_commands_with_failover_retry(self, mock_sentinel_class): + """Test Redis commands with failover retry mechanism""" + mock_sentinel = Mock() + mock_master = Mock() + + # First call fails with connection error, second succeeds + mock_master.hget.side_effect = [ + redis.exceptions.ConnectionError("Connection failed"), + "recovered_value", + ] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False) + + # Test hget with retry + hget_method = proxy.__getattr__("hget") + result = hget_method("test_hash", "field1") + + assert result == "recovered_value" + assert mock_master.hget.call_count == 2 + + # Verify both calls were made with same parameters + expected_calls = [(("test_hash", "field1"),), (("test_hash", "field1"),)] + actual_calls = [call.args for call in mock_master.hget.call_args_list] + assert actual_calls == expected_calls + + @patch("redis.sentinel.Sentinel") + def test_commands_with_readonly_error_retry(self, mock_sentinel_class): + """Test Redis commands with ReadOnlyError retry mechanism""" + mock_sentinel = Mock() + mock_master = Mock() + + # First call fails with ReadOnlyError, second succeeds + mock_master.hset.side_effect = [ + redis.exceptions.ReadOnlyError( + "READONLY You can't write against a read only replica" + ), + 1, + ] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False) + + # Test hset with retry + hset_method = proxy.__getattr__("hset") + result = hset_method("test_hash", "field1", "value1") + + assert result == 1 + assert mock_master.hset.call_count == 2 + + # Verify both calls were made with same parameters + expected_calls = [ + (("test_hash", "field1", "value1"),), + (("test_hash", "field1", "value1"),), + ] + actual_calls = [call.args for call in mock_master.hset.call_args_list] + assert actual_calls == expected_calls + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_async_commands_with_failover_retry(self, mock_sentinel_class): + """Test async Redis commands with failover retry mechanism""" + mock_sentinel = Mock() + mock_master = Mock() + + # First call fails with connection error, second succeeds + mock_master.hget = AsyncMock( + side_effect=[ + redis.exceptions.ConnectionError("Connection failed"), + "recovered_value", + ] + ) + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Test async hget with retry + hget_method = proxy.__getattr__("hget") + result = await hget_method("test_hash", "field1") + + assert result == "recovered_value" + assert mock_master.hget.call_count == 2 + + # Verify both calls were made with same parameters + expected_calls = [(("test_hash", "field1"),), (("test_hash", "field1"),)] + actual_calls = [call.args for call in mock_master.hget.call_args_list] + assert actual_calls == expected_calls + + +class TestSentinelRedisProxyFactoryMethods: + """Test Redis factory methods in async mode - these are special cases that remain sync""" + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_pubsub_factory_method_async(self, mock_sentinel_class): + """Test pubsub factory method in async mode - should pass through without wrapping""" + mock_sentinel = Mock() + mock_master = Mock() + mock_pubsub = Mock() + + # Mock pubsub factory method + mock_master.pubsub.return_value = mock_pubsub + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Test pubsub factory method - should NOT be wrapped as async + pubsub_method = proxy.__getattr__("pubsub") + result = pubsub_method() + + assert result == mock_pubsub + mock_master.pubsub.assert_called_once() + + # Verify it's not wrapped as async (no await needed) + assert not inspect.iscoroutine(result) + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_pipeline_factory_method_async(self, mock_sentinel_class): + """Test pipeline factory method in async mode - should pass through without wrapping""" + mock_sentinel = Mock() + mock_master = Mock() + mock_pipeline = Mock() + + # Mock pipeline factory method + mock_master.pipeline.return_value = mock_pipeline + mock_pipeline.set.return_value = mock_pipeline + mock_pipeline.get.return_value = mock_pipeline + mock_pipeline.execute.return_value = [True, "test_value"] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Test pipeline factory method - should NOT be wrapped as async + pipeline_method = proxy.__getattr__("pipeline") + result = pipeline_method() + + assert result == mock_pipeline + mock_master.pipeline.assert_called_once() + + # Verify it's not wrapped as async (no await needed) + assert not inspect.iscoroutine(result) + + # Test pipeline usage (these should also be sync) + pipeline_result = result.set("key", "value").get("key").execute() + assert pipeline_result == [True, "test_value"] + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_factory_methods_vs_regular_commands_async(self, mock_sentinel_class): + """Test that factory methods behave differently from regular commands in async mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock both factory method and regular command + mock_pubsub = Mock() + mock_master.pubsub.return_value = mock_pubsub + mock_master.get = AsyncMock(return_value="test_value") + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Test factory method - should NOT be wrapped + pubsub_method = proxy.__getattr__("pubsub") + pubsub_result = pubsub_method() + + # Test regular command - should be wrapped as async + get_method = proxy.__getattr__("get") + get_result = get_method("test_key") + + # Factory method returns directly + assert pubsub_result == mock_pubsub + assert not inspect.iscoroutine(pubsub_result) + + # Regular command returns coroutine + assert inspect.iscoroutine(get_result) + + # Regular command needs await + actual_value = await get_result + assert actual_value == "test_value" + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_factory_methods_with_failover_async(self, mock_sentinel_class): + """Test factory methods with failover in async mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # First call fails, second succeeds + mock_pubsub = Mock() + mock_master.pubsub.side_effect = [ + redis.exceptions.ConnectionError("Connection failed"), + mock_pubsub, + ] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Test pubsub factory method with failover + pubsub_method = proxy.__getattr__("pubsub") + result = pubsub_method() + + assert result == mock_pubsub + assert mock_master.pubsub.call_count == 2 # Retry happened + + # Verify it's still not wrapped as async after retry + assert not inspect.iscoroutine(result) + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_monitor_factory_method_async(self, mock_sentinel_class): + """Test monitor factory method in async mode - should pass through without wrapping""" + mock_sentinel = Mock() + mock_master = Mock() + mock_monitor = Mock() + + # Mock monitor factory method + mock_master.monitor.return_value = mock_monitor + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Test monitor factory method - should NOT be wrapped as async + monitor_method = proxy.__getattr__("monitor") + result = monitor_method() + + assert result == mock_monitor + mock_master.monitor.assert_called_once() + + # Verify it's not wrapped as async (no await needed) + assert not inspect.iscoroutine(result) + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_client_factory_method_async(self, mock_sentinel_class): + """Test client factory method in async mode - should pass through without wrapping""" + mock_sentinel = Mock() + mock_master = Mock() + mock_client = Mock() + + # Mock client factory method + mock_master.client.return_value = mock_client + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Test client factory method - should NOT be wrapped as async + client_method = proxy.__getattr__("client") + result = client_method() + + assert result == mock_client + mock_master.client.assert_called_once() + + # Verify it's not wrapped as async (no await needed) + assert not inspect.iscoroutine(result) + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_transaction_factory_method_async(self, mock_sentinel_class): + """Test transaction factory method in async mode - should pass through without wrapping""" + mock_sentinel = Mock() + mock_master = Mock() + mock_transaction = Mock() + + # Mock transaction factory method + mock_master.transaction.return_value = mock_transaction + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Test transaction factory method - should NOT be wrapped as async + transaction_method = proxy.__getattr__("transaction") + result = transaction_method() + + assert result == mock_transaction + mock_master.transaction.assert_called_once() + + # Verify it's not wrapped as async (no await needed) + assert not inspect.iscoroutine(result) + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_all_factory_methods_async(self, mock_sentinel_class): + """Test all factory methods in async mode - comprehensive test""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock all factory methods + mock_objects = { + "pipeline": Mock(), + "pubsub": Mock(), + "monitor": Mock(), + "client": Mock(), + "transaction": Mock(), + } + + for method_name, mock_obj in mock_objects.items(): + setattr(mock_master, method_name, Mock(return_value=mock_obj)) + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Test all factory methods + for method_name, expected_obj in mock_objects.items(): + method = proxy.__getattr__(method_name) + result = method() + + assert result == expected_obj + assert not inspect.iscoroutine(result) + getattr(mock_master, method_name).assert_called_once() + + # Reset mock for next iteration + getattr(mock_master, method_name).reset_mock() + + @patch("redis.sentinel.Sentinel") + @pytest.mark.asyncio + async def test_mixed_factory_and_regular_commands_async(self, mock_sentinel_class): + """Test using both factory methods and regular commands in async mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock pipeline factory and regular commands + mock_pipeline = Mock() + mock_master.pipeline.return_value = mock_pipeline + mock_pipeline.set.return_value = mock_pipeline + mock_pipeline.get.return_value = mock_pipeline + mock_pipeline.execute.return_value = [True, "pipeline_value"] + + mock_master.get = AsyncMock(return_value="regular_value") + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True) + + # Use factory method (sync) + pipeline = proxy.__getattr__("pipeline")() + pipeline_result = pipeline.set("key1", "value1").get("key1").execute() + + # Use regular command (async) + get_method = proxy.__getattr__("get") + regular_result = await get_method("key2") + + # Verify both work correctly + assert pipeline_result == [True, "pipeline_value"] + assert regular_result == "regular_value" + + # Verify calls + mock_master.pipeline.assert_called_once() + mock_master.get.assert_called_with("key2") diff --git a/backend/open_webui/utils/redis.py b/backend/open_webui/utils/redis.py index 22a86f4796..195bc951d0 100644 --- a/backend/open_webui/utils/redis.py +++ b/backend/open_webui/utils/redis.py @@ -6,6 +6,7 @@ import redis MAX_RETRY_COUNT = 2 + class SentinelRedisProxy: def __init__(self, sentinel, service, *, async_mode: bool = True, **kw): self._sentinel = sentinel