Skip to content

Commit 871ca8f

Browse files
committed
[GROW-3599] don't call RedisCluster.initialize on empty ConnectionPool (#18)
1 parent fa17c77 commit 871ca8f

File tree

5 files changed

+85
-8
lines changed

5 files changed

+85
-8
lines changed

redis/cluster.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
ConnectionError,
2222
DataError,
2323
MasterDownError,
24+
MaxConnectionsError,
2425
MovedError,
2526
RedisClusterException,
2627
RedisError,
@@ -386,7 +387,12 @@ class AbstractRedisCluster:
386387
list_keys_to_dict(["SCRIPT FLUSH"], lambda command, res: all(res.values())),
387388
)
388389

389-
ERRORS_ALLOW_RETRY = (ConnectionError, TimeoutError, ClusterDownError)
390+
ERRORS_ALLOW_RETRY = (
391+
ClusterDownError,
392+
ConnectionError,
393+
MaxConnectionsError,
394+
TimeoutError,
395+
)
390396

391397
def replace_default_node(self, target_node: "ClusterNode" = None) -> None:
392398
"""Replace the default cluster node.
@@ -1142,7 +1148,7 @@ def _execute_command(self, target_node, *args, **kwargs):
11421148
response, **kwargs
11431149
)
11441150
return response
1145-
except AuthenticationError:
1151+
except (AuthenticationError, MaxConnectionsError):
11461152
raise
11471153
except (ConnectionError, TimeoutError) as e:
11481154
# Connection retries are being handled in the node's
@@ -2034,7 +2040,12 @@ def send_cluster_commands(
20342040
allow_redirections=allow_redirections,
20352041
attempts_count=self.cluster_error_retry_attempts - retry_attempts,
20362042
)
2037-
except (ClusterDownError, ConnectionError, TimeoutError) as e:
2043+
except (
2044+
ClusterDownError,
2045+
ConnectionError,
2046+
MaxConnectionsError,
2047+
TimeoutError,
2048+
) as e:
20382049
if retry_attempts > 0:
20392050
# Try again with the new cluster setup. All other errors
20402051
# should be raised.
@@ -2109,7 +2120,7 @@ def _send_cluster_commands(
21092120
backoff = self.retry._backoff.compute(attempts_count)
21102121
if backoff > 0:
21112122
time.sleep(backoff)
2112-
if isinstance(e, (ConnectionError, TimeoutError)):
2123+
if type(e) in (ConnectionError, TimeoutError):
21132124
self.nodes_manager.initialize()
21142125
if is_default_node:
21152126
self.replace_default_node()

redis/connection.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
DataError,
2323
ExecAbortError,
2424
InvalidResponse,
25+
MaxConnectionsError,
2526
ModuleError,
2627
NoPermissionError,
2728
NoScriptError,
@@ -1471,7 +1472,7 @@ def get_encoder(self):
14711472
def make_connection(self):
14721473
"Create a new connection"
14731474
if self._created_connections >= self.max_connections:
1474-
raise ConnectionError("Too many connections")
1475+
raise MaxConnectionsError("Too many connections")
14751476
self._created_connections += 1
14761477
return self.connection_class(**self.connection_kwargs)
14771478

@@ -1631,7 +1632,7 @@ def get_connection(self, command_name, *keys, **options):
16311632
except Empty:
16321633
# Note that this is not caught by the redis client and will be
16331634
# raised unless handled by application code. If you want never to
1634-
raise ConnectionError("No connection available.")
1635+
raise MaxConnectionsError("No connection available.")
16351636

16361637
# If the ``connection`` is actually ``None`` then that's a cue to make
16371638
# a new connection to add to the pool.

redis/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,9 @@ class SlotNotCoveredError(RedisClusterException):
202202

203203

204204
class MaxConnectionsError(ConnectionError):
205+
"""
206+
Indicates that a connection pool ran out of connections and
207+
can't create more due to maximum connection count limitation
208+
"""
209+
205210
...

tests/test_cluster.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
from redis.crc import key_slot
3434
from redis.exceptions import (
3535
AskError,
36+
AuthenticationError,
3637
ClusterDownError,
3738
ConnectionError,
3839
DataError,
40+
MaxConnectionsError,
3941
MovedError,
4042
NoPermissionError,
4143
RedisClusterException,
@@ -821,6 +823,21 @@ def raise_error(target_node, *args, **kwargs):
821823
rc.get("bar")
822824
assert compute.call_count == rc.cluster_error_retry_attempts
823825

826+
@pytest.mark.parametrize("error", [AuthenticationError, MaxConnectionsError])
827+
def test_skip_initialize(self, r, error):
828+
for n in r.nodes_manager.nodes_cache.values():
829+
n.redis_connection.connection_pool.max_connections = 3
830+
for _ in range(0, n.redis_connection.connection_pool.max_connections):
831+
n.redis_connection.connection_pool.get_connection("GET")
832+
833+
with patch.object(NodesManager, "initialize") as i:
834+
with pytest.raises(MaxConnectionsError):
835+
r.get("a")
836+
assert i.call_count == 0
837+
838+
for n in r.nodes_manager.nodes_cache.values():
839+
n.redis_connection.connection_pool.reset()
840+
824841
@pytest.mark.parametrize("reinitialize_steps", [2, 10, 99])
825842
def test_recover_slot_not_covered_error(self, request, reinitialize_steps):
826843
rc = _get_client(RedisCluster, request, reinitialize_steps=reinitialize_steps)
@@ -3079,7 +3096,49 @@ def test_empty_stack(self, r):
30793096
result = p.execute()
30803097
assert result == []
30813098

3099+
@pytest.mark.parametrize("error", [AuthenticationError, MaxConnectionsError])
3100+
def test_error_does_not_trigger_initialize(self, r, error):
3101+
with patch("redis.cluster.get_connection") as get_connection:
3102+
3103+
def raise_error(target_node, *args, **kwargs):
3104+
get_connection.failed_calls += 1
3105+
raise error("mocked error")
3106+
3107+
get_connection.side_effect = raise_error
3108+
3109+
r.set_retry(Retry(ConstantBackoff(0.1), 5))
3110+
pipeline = r.pipeline()
3111+
3112+
with patch.object(NodesManager, "initialize") as i:
3113+
with pytest.raises(error):
3114+
pipeline.get("bar")
3115+
pipeline.get("bar")
3116+
pipeline.execute()
3117+
assert i.call_count == 0
3118+
30823119
@pytest.mark.parametrize("error", [ConnectionError, TimeoutError])
3120+
def test_error_trigger_initialize(self, r, error):
3121+
with patch("redis.cluster.get_connection") as get_connection:
3122+
3123+
def raise_error(target_node, *args, **kwargs):
3124+
get_connection.failed_calls += 1
3125+
raise error("mocked error")
3126+
3127+
get_connection.side_effect = raise_error
3128+
3129+
r.set_retry(Retry(ConstantBackoff(0.1), 5))
3130+
pipeline = r.pipeline()
3131+
3132+
with patch.object(NodesManager, "initialize") as i:
3133+
with pytest.raises(error):
3134+
pipeline.get("bar")
3135+
pipeline.get("bar")
3136+
pipeline.execute()
3137+
assert i.call_count == r.cluster_error_retry_attempts + 1
3138+
3139+
@pytest.mark.parametrize(
3140+
"error", [ConnectionError, TimeoutError, MaxConnectionsError]
3141+
)
30833142
def test_additional_backoff_cluster_pipeline(self, r, error):
30843143
with patch.object(ConstantBackoff, "compute") as compute:
30853144

tests/test_connection_pool.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import redis
1010
from redis.connection import ssl_available, to_bool
11+
from redis.exceptions import MaxConnectionsError
1112

1213
from .conftest import _get_client, skip_if_redis_enterprise, skip_if_server_version_lt
1314
from .test_pubsub import wait_for_message
@@ -63,7 +64,7 @@ def test_max_connections(self, master_host):
6364
pool = self.get_pool(max_connections=2, connection_kwargs=connection_kwargs)
6465
pool.get_connection("_")
6566
pool.get_connection("_")
66-
with pytest.raises(redis.ConnectionError):
67+
with pytest.raises(MaxConnectionsError):
6768
pool.get_connection("_")
6869

6970
def test_reuse_previously_released_connection(self, master_host):
@@ -142,7 +143,7 @@ def test_connection_pool_blocks_until_timeout(self, master_host):
142143
pool.get_connection("_")
143144

144145
start = time.time()
145-
with pytest.raises(redis.ConnectionError):
146+
with pytest.raises(MaxConnectionsError):
146147
pool.get_connection("_")
147148
# we should have waited at least 0.1 seconds
148149
assert time.time() - start >= 0.1

0 commit comments

Comments
 (0)