Skip to content

Commit 76c74a0

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 9e93f68 commit 76c74a0

File tree

3 files changed

+242
-6
lines changed

3 files changed

+242
-6
lines changed

redisvl/redis/connection.py

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os
2-
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, overload
3-
from urllib.parse import urlparse
2+
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union, overload
3+
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
44
from warnings import warn
55

66
from redis import Redis, RedisCluster
@@ -21,6 +21,52 @@
2121
from redisvl.utils.utils import deprecated_function
2222

2323

24+
def _strip_cluster_from_url_and_kwargs(
25+
url: str, **kwargs
26+
) -> Tuple[str, Dict[str, Any]]:
27+
"""Remove 'cluster' parameter from URL query string and kwargs.
28+
29+
AsyncRedisCluster doesn't accept 'cluster' parameter, but it might be
30+
present in the URL or kwargs for compatibility with other Redis clients.
31+
32+
Args:
33+
url: Redis URL that might contain cluster parameter
34+
**kwargs: Keyword arguments that might contain cluster parameter
35+
36+
Returns:
37+
Tuple of (cleaned_url, cleaned_kwargs)
38+
"""
39+
# Parse the URL
40+
parsed = urlparse(url)
41+
42+
# Parse query parameters
43+
query_params = parse_qs(parsed.query)
44+
45+
# Remove 'cluster' parameter if present
46+
query_params.pop("cluster", None)
47+
48+
# Reconstruct the query string
49+
new_query = urlencode(query_params, doseq=True)
50+
51+
# Reconstruct the URL
52+
cleaned_url = urlunparse(
53+
(
54+
parsed.scheme,
55+
parsed.netloc,
56+
parsed.path,
57+
parsed.params,
58+
new_query,
59+
parsed.fragment,
60+
)
61+
)
62+
63+
# Remove 'cluster' from kwargs if present
64+
cleaned_kwargs = kwargs.copy()
65+
cleaned_kwargs.pop("cluster", None)
66+
67+
return cleaned_url, cleaned_kwargs
68+
69+
2470
def compare_versions(version1: str, version2: str):
2571
"""
2672
Compare two Redis version strings numerically.
@@ -311,9 +357,17 @@ async def _get_aredis_connection(
311357
url, AsyncRedis, **kwargs
312358
)
313359
elif is_cluster_url(url, **kwargs):
314-
client = AsyncRedisCluster.from_url(url, **kwargs)
360+
# Strip 'cluster' parameter as AsyncRedisCluster doesn't accept it
361+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(
362+
url, **kwargs
363+
)
364+
client = AsyncRedisCluster.from_url(cleaned_url, **cleaned_kwargs)
315365
else:
316-
client = AsyncRedis.from_url(url, **kwargs)
366+
# Also strip cluster parameter for AsyncRedis to avoid connection issues
367+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(
368+
url, **kwargs
369+
)
370+
client = AsyncRedis.from_url(cleaned_url, **cleaned_kwargs)
317371

318372
# Module validation removed - operations will fail naturally if modules are missing
319373
# Set client library name only
@@ -351,11 +405,23 @@ def get_async_redis_connection(
351405
DeprecationWarning,
352406
)
353407
url = url or get_address_from_env()
408+
354409
if url.startswith("redis+sentinel"):
355410
return RedisConnectionFactory._redis_sentinel_client(
356411
url, AsyncRedis, **kwargs
357412
)
358-
return AsyncRedis.from_url(url, **kwargs)
413+
elif is_cluster_url(url, **kwargs):
414+
# Strip 'cluster' parameter as AsyncRedisCluster doesn't accept it
415+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(
416+
url, **kwargs
417+
)
418+
return AsyncRedisCluster.from_url(cleaned_url, **cleaned_kwargs)
419+
else:
420+
# Also strip cluster parameter for AsyncRedis to avoid connection issues
421+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(
422+
url, **kwargs
423+
)
424+
return AsyncRedis.from_url(cleaned_url, **cleaned_kwargs)
359425

360426
@staticmethod
361427
def get_redis_cluster_connection(
@@ -373,7 +439,9 @@ def get_async_redis_cluster_connection(
373439
) -> AsyncRedisCluster:
374440
"""Creates and returns an asynchronous Redis client for a Redis cluster."""
375441
url = redis_url or get_address_from_env()
376-
return AsyncRedisCluster.from_url(url, **kwargs)
442+
# Strip 'cluster' parameter as AsyncRedisCluster doesn't accept it
443+
cleaned_url, cleaned_kwargs = _strip_cluster_from_url_and_kwargs(url, **kwargs)
444+
return AsyncRedisCluster.from_url(cleaned_url, **cleaned_kwargs)
377445

378446
@staticmethod
379447
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)