Skip to content

Commit bec0b81

Browse files
committed
fix: Handle cluster parameter in AsyncRedisCluster connections (#346)
AsyncRedisCluster.from_url() doesn't accept 'cluster' parameter but it might be present in URLs or kwargs for compatibility. This fix strips the cluster parameter before creating async Redis connections. Changes: - Add _strip_cluster_from_url_and_kwargs helper function - Update _get_aredis_connection to strip cluster parameter - Update get_async_redis_cluster_connection to strip cluster parameter - Update deprecated get_async_redis_connection to handle both cluster types - Strip cluster parameter for both AsyncRedis and AsyncRedisCluster
1 parent 2660e2f commit bec0b81

File tree

3 files changed

+243
-5
lines changed

3 files changed

+243
-5
lines changed

redisvl/redis/connection.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
2-
from typing import Any, Dict, List, Optional, Type
2+
from typing import Any, Dict, List, Optional, Tuple, Type
3+
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
34
from warnings import warn
45

56
from redis import Redis, RedisCluster
@@ -19,6 +20,52 @@
1920
from redisvl.utils.utils import deprecated_function
2021

2122

23+
def _strip_cluster_from_url_and_kwargs(
24+
url: str, **kwargs
25+
) -> Tuple[str, Dict[str, Any]]:
26+
"""Remove 'cluster' parameter from URL query string and kwargs.
27+
28+
AsyncRedisCluster doesn't accept 'cluster' parameter, but it might be
29+
present in the URL or kwargs for compatibility with other Redis clients.
30+
31+
Args:
32+
url: Redis URL that might contain cluster parameter
33+
**kwargs: Keyword arguments that might contain cluster parameter
34+
35+
Returns:
36+
Tuple of (cleaned_url, cleaned_kwargs)
37+
"""
38+
# Parse the URL
39+
parsed = urlparse(url)
40+
41+
# Parse query parameters
42+
query_params = parse_qs(parsed.query)
43+
44+
# Remove 'cluster' parameter if present
45+
query_params.pop("cluster", None)
46+
47+
# Reconstruct the query string
48+
new_query = urlencode(query_params, doseq=True)
49+
50+
# Reconstruct the URL
51+
cleaned_url = urlunparse(
52+
(
53+
parsed.scheme,
54+
parsed.netloc,
55+
parsed.path,
56+
parsed.params,
57+
new_query,
58+
parsed.fragment,
59+
)
60+
)
61+
62+
# Remove 'cluster' from kwargs if present
63+
cleaned_kwargs = kwargs.copy()
64+
cleaned_kwargs.pop("cluster", None)
65+
66+
return cleaned_url, cleaned_kwargs
67+
68+
2269
def compare_versions(version1: str, version2: str):
2370
"""
2471
Compare two Redis version strings numerically.
@@ -300,9 +347,17 @@ async def _get_aredis_connection(
300347
url = url or get_address_from_env()
301348

302349
if is_cluster_url(url, **kwargs):
303-
client = AsyncRedisCluster.from_url(url, **kwargs)
350+
# Strip 'cluster' parameter as AsyncRedisCluster doesn't accept it
351+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(
352+
url, **kwargs
353+
)
354+
client = AsyncRedisCluster.from_url(cleaned_url, **cleaned_kwargs)
304355
else:
305-
client = AsyncRedis.from_url(url, **kwargs)
356+
# Also strip cluster parameter for AsyncRedis to avoid connection issues
357+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(
358+
url, **kwargs
359+
)
360+
client = AsyncRedis.from_url(cleaned_url, **cleaned_kwargs)
306361

307362
# Module validation removed - operations will fail naturally if modules are missing
308363
# Set client library name only
@@ -340,7 +395,20 @@ def get_async_redis_connection(
340395
DeprecationWarning,
341396
)
342397
url = url or get_address_from_env()
343-
return AsyncRedis.from_url(url, **kwargs)
398+
399+
# Handle both cluster and non-cluster URLs
400+
if is_cluster_url(url, **kwargs):
401+
# Strip 'cluster' parameter as AsyncRedisCluster doesn't accept it
402+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(
403+
url, **kwargs
404+
)
405+
return AsyncRedisCluster.from_url(cleaned_url, **cleaned_kwargs)
406+
else:
407+
# Also strip cluster parameter for AsyncRedis to avoid connection issues
408+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(
409+
url, **kwargs
410+
)
411+
return AsyncRedis.from_url(cleaned_url, **cleaned_kwargs)
344412

345413
@staticmethod
346414
def get_redis_cluster_connection(
@@ -358,7 +426,9 @@ def get_async_redis_cluster_connection(
358426
) -> AsyncRedisCluster:
359427
"""Creates and returns an asynchronous Redis client for a Redis cluster."""
360428
url = redis_url or get_address_from_env()
361-
return AsyncRedisCluster.from_url(url, **kwargs)
429+
# Strip 'cluster' parameter as AsyncRedisCluster doesn't accept it
430+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(url, **kwargs)
431+
return AsyncRedisCluster.from_url(cleaned_url, **cleaned_kwargs)
362432

363433
@staticmethod
364434
def sync_to_async_redis(
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Integration tests for AsyncRedisCluster connection with cluster parameter (issue #346)."""
2+
3+
import pytest
4+
from redis.asyncio import Redis as AsyncRedis
5+
from redis.asyncio.cluster import RedisCluster as AsyncRedisCluster
6+
7+
from redisvl.redis.connection import RedisConnectionFactory
8+
9+
10+
@pytest.mark.asyncio
11+
class TestAsyncClusterConnection:
12+
"""Test AsyncRedisCluster connections with cluster parameter."""
13+
14+
async def test_get_aredis_connection_with_cluster_url(self, redis_url):
15+
"""Test _get_aredis_connection handles cluster parameter in URL."""
16+
# Add cluster=true to the URL (even though it's not actually a cluster)
17+
# This simulates the issue where cluster=true is passed but not accepted
18+
cluster_url = (
19+
f"{redis_url}?cluster=false" # Use false since we don't have a real cluster
20+
)
21+
22+
# This should not raise a TypeError
23+
client = await RedisConnectionFactory._get_aredis_connection(cluster_url)
24+
25+
assert client is not None
26+
assert isinstance(client, (AsyncRedis, AsyncRedisCluster))
27+
28+
await client.aclose()
29+
30+
async def test_get_aredis_connection_with_cluster_kwargs(self, redis_url):
31+
"""Test _get_aredis_connection handles cluster parameter in kwargs."""
32+
# This should not raise a TypeError even with cluster in kwargs
33+
client = await RedisConnectionFactory._get_aredis_connection(
34+
redis_url, cluster=False # Use false since we don't have a real cluster
35+
)
36+
37+
assert client is not None
38+
assert isinstance(client, (AsyncRedis, AsyncRedisCluster))
39+
40+
await client.aclose()
41+
42+
@pytest.mark.requires_cluster
43+
async def test_get_async_redis_cluster_connection_with_params(
44+
self, redis_cluster_url
45+
):
46+
"""Test get_async_redis_cluster_connection with cluster parameter."""
47+
# Add cluster=true to the URL
48+
cluster_url_with_param = f"{redis_cluster_url}?cluster=true"
49+
50+
# This should not raise a TypeError
51+
client = RedisConnectionFactory.get_async_redis_cluster_connection(
52+
cluster_url_with_param
53+
)
54+
55+
assert client is not None
56+
assert isinstance(client, AsyncRedisCluster)
57+
58+
await client.aclose()
59+
60+
@pytest.mark.requires_cluster
61+
async def test_get_async_redis_cluster_connection_with_kwargs(
62+
self, redis_cluster_url
63+
):
64+
"""Test get_async_redis_cluster_connection with cluster in kwargs."""
65+
# This should not raise a TypeError
66+
client = RedisConnectionFactory.get_async_redis_cluster_connection(
67+
redis_cluster_url, cluster=True
68+
)
69+
70+
assert client is not None
71+
assert isinstance(client, AsyncRedisCluster)
72+
73+
await client.aclose()
74+
75+
def test_get_async_redis_connection_deprecated_with_cluster(self, redis_url):
76+
"""Test deprecated get_async_redis_connection handles cluster parameter."""
77+
# Add cluster=false to the URL
78+
cluster_url = f"{redis_url}?cluster=false"
79+
80+
with pytest.warns(DeprecationWarning):
81+
# This should not raise a TypeError
82+
client = RedisConnectionFactory.get_async_redis_connection(cluster_url)
83+
84+
assert client is not None
85+
assert isinstance(client, (AsyncRedis, AsyncRedisCluster))
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Unit tests for AsyncRedisCluster cluster parameter stripping fix (issue #346)."""
2+
3+
import pytest
4+
5+
from redisvl.redis.connection import _strip_cluster_from_url_and_kwargs
6+
7+
8+
class TestAsyncClusterParameterStripping:
9+
"""Test the helper function that strips cluster parameter from URLs and kwargs."""
10+
11+
def test_strip_cluster_from_url_with_cluster_true(self):
12+
"""Test stripping cluster=true from URL query string."""
13+
url = "redis://localhost:7001?cluster=true"
14+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(url)
15+
16+
assert cleaned_url == "redis://localhost:7001"
17+
assert cleaned_kwargs == {}
18+
19+
def test_strip_cluster_from_url_with_other_params(self):
20+
"""Test stripping cluster parameter while preserving other parameters."""
21+
url = (
22+
"redis://localhost:7001?cluster=true&decode_responses=true&socket_timeout=5"
23+
)
24+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(
25+
url, some_kwarg="value"
26+
)
27+
28+
assert "cluster" not in cleaned_url
29+
assert "decode_responses=true" in cleaned_url
30+
assert "socket_timeout=5" in cleaned_url
31+
assert cleaned_kwargs == {"some_kwarg": "value"}
32+
33+
def test_strip_cluster_from_kwargs(self):
34+
"""Test stripping cluster parameter from kwargs."""
35+
url = "redis://localhost:7001"
36+
kwargs = {"cluster": True, "decode_responses": True}
37+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(url, **kwargs)
38+
39+
assert cleaned_url == "redis://localhost:7001"
40+
assert "cluster" not in cleaned_kwargs
41+
assert cleaned_kwargs == {"decode_responses": True}
42+
43+
def test_strip_cluster_from_both_url_and_kwargs(self):
44+
"""Test stripping cluster parameter from both URL and kwargs."""
45+
url = "redis://localhost:7001?cluster=true"
46+
kwargs = {"cluster": True, "socket_timeout": 5}
47+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(url, **kwargs)
48+
49+
assert cleaned_url == "redis://localhost:7001"
50+
assert cleaned_kwargs == {"socket_timeout": 5}
51+
52+
def test_no_cluster_parameter_unchanged(self):
53+
"""Test that URLs and kwargs without cluster parameter remain unchanged."""
54+
url = "redis://localhost:7001?decode_responses=true"
55+
kwargs = {"socket_timeout": 5}
56+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(url, **kwargs)
57+
58+
assert cleaned_url == "redis://localhost:7001?decode_responses=true"
59+
assert cleaned_kwargs == {"socket_timeout": 5}
60+
61+
def test_empty_url_query_and_kwargs(self):
62+
"""Test handling of URL without query string and empty kwargs."""
63+
url = "redis://localhost:7001"
64+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(url)
65+
66+
assert cleaned_url == "redis://localhost:7001"
67+
assert cleaned_kwargs == {}
68+
69+
def test_complex_url_with_auth_and_db(self):
70+
"""Test complex URL with authentication and database selection."""
71+
url = "redis://user:password@localhost:7001/0?cluster=true&socket_timeout=5"
72+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(url)
73+
74+
assert cleaned_url == "redis://user:password@localhost:7001/0?socket_timeout=5"
75+
assert cleaned_kwargs == {}
76+
77+
def test_cluster_false_also_stripped(self):
78+
"""Test that cluster=false is also stripped (any cluster param should be removed)."""
79+
url = "redis://localhost:7001?cluster=false"
80+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(url)
81+
82+
assert cleaned_url == "redis://localhost:7001"
83+
assert cleaned_kwargs == {}

0 commit comments

Comments
 (0)