Skip to content

Commit f8c16e2

Browse files
Fix ConnectionPool to raise MaxConnectionsError instead of ConnectionError
- Added MaxConnectionsError class as a subclass of ConnectionError - Updated connection.py to raise the more specific error - Updated cluster.py to handle this specific error type - Added tests to verify the behavior Fixes #3684
1 parent 950a462 commit f8c16e2

File tree

6 files changed

+121
-6
lines changed

6 files changed

+121
-6
lines changed

redis/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
ConnectionError,
1919
DataError,
2020
InvalidResponse,
21+
MaxConnectionsError,
2122
OutOfMemoryError,
2223
PubSubError,
2324
ReadOnlyError,
@@ -60,6 +61,7 @@ def int_or_str(value):
6061
"from_url",
6162
"default_backoff",
6263
"InvalidResponse",
64+
"MaxConnectionsError",
6365
"OutOfMemoryError",
6466
"PubSubError",
6567
"ReadOnlyError",

redis/cluster.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
ClusterError,
3030
ConnectionError,
3131
DataError,
32+
MaxConnectionsError,
3233
MovedError,
3334
RedisClusterException,
3435
RedisError,
@@ -1200,6 +1201,12 @@ def _execute_command(self, target_node, *args, **kwargs):
12001201
return response
12011202
except AuthenticationError:
12021203
raise
1204+
except MaxConnectionsError:
1205+
# MaxConnectionsError indicates client-side resource exhaustion
1206+
# (too many connections in the pool), not a node failure.
1207+
# Don't treat this as a node failure - just re-raise the error
1208+
# without reinitializing the cluster.
1209+
raise
12031210
except (ConnectionError, TimeoutError) as e:
12041211
# Connection retries are being handled in the node's
12051212
# Retry object.

redis/connection.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
ChildDeadlockedError,
3232
ConnectionError,
3333
DataError,
34+
MaxConnectionsError,
3435
RedisError,
3536
ResponseError,
3637
TimeoutError,
@@ -1550,7 +1551,7 @@ def get_encoder(self) -> Encoder:
15501551
def make_connection(self) -> "ConnectionInterface":
15511552
"Create a new connection"
15521553
if self._created_connections >= self.max_connections:
1553-
raise ConnectionError("Too many connections")
1554+
raise MaxConnectionsError("Too many connections")
15541555
self._created_connections += 1
15551556

15561557
if self.cache is not None:

redis/exceptions.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,9 @@ class SlotNotCoveredError(RedisClusterException):
220220
pass
221221

222222

223-
class MaxConnectionsError(ConnectionError): ...
223+
class MaxConnectionsError(ConnectionError):
224+
"""
225+
Raised when a connection pool has reached its max_connections limit.
226+
This indicates pool exhaustion rather than an actual connection failure.
227+
"""
228+
pass

tests/test_connection_pool.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,22 @@ class DummyConnection:
2626
def __init__(self, **kwargs):
2727
self.kwargs = kwargs
2828
self.pid = os.getpid()
29+
self.connected = False
2930

3031
def connect(self):
31-
pass
32+
self.connected = True
3233

34+
def disconnect(self):
35+
self.connected = False
36+
3337
def can_read(self):
3438
return False
39+
40+
def send_command(self, *args, **kwargs):
41+
pass
42+
43+
def read_response(self, disable_decoding=False, **kwargs):
44+
return "PONG"
3545

3646

3747
class TestConnectionPool:
@@ -76,11 +86,13 @@ def test_multiple_connections(self, master_host):
7686
assert c1 != c2
7787

7888
def test_max_connections(self, master_host):
79-
connection_kwargs = {"host": master_host[0], "port": master_host[1]}
80-
pool = self.get_pool(max_connections=2, connection_kwargs=connection_kwargs)
89+
# Use DummyConnection to avoid actual connection to Redis
90+
# This prevents authentication issues and makes the test more reliable
91+
# while still properly testing the MaxConnectionsError behavior
92+
pool = self.get_pool(max_connections=2, connection_class=DummyConnection)
8193
pool.get_connection()
8294
pool.get_connection()
83-
with pytest.raises(redis.ConnectionError):
95+
with pytest.raises(redis.MaxConnectionsError):
8496
pool.get_connection()
8597

8698
def test_reuse_previously_released_connection(self, master_host):

tests/test_max_connections_error.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import pytest
2+
import redis
3+
from unittest import mock
4+
from redis.connection import ConnectionInterface
5+
6+
7+
class DummyConnection(ConnectionInterface):
8+
"""A dummy connection class for testing that doesn't actually connect to Redis"""
9+
def __init__(self, *args, **kwargs):
10+
self.connected = False
11+
12+
def connect(self):
13+
self.connected = True
14+
15+
def disconnect(self):
16+
self.connected = False
17+
18+
def register_connect_callback(self, callback): pass
19+
def deregister_connect_callback(self, callback): pass
20+
def set_parser(self, parser_class): pass
21+
def get_protocol(self): return 2
22+
def on_connect(self): pass
23+
def check_health(self): return True
24+
def send_packed_command(self, command, check_health=True): pass
25+
def send_command(self, *args, **kwargs): pass
26+
def can_read(self, timeout=0): return False
27+
def read_response(self, disable_decoding=False, **kwargs): return "PONG"
28+
29+
30+
@pytest.mark.onlynoncluster
31+
def test_max_connections_error_inheritance():
32+
"""Test that MaxConnectionsError is a subclass of ConnectionError"""
33+
assert issubclass(redis.MaxConnectionsError, redis.ConnectionError)
34+
35+
36+
@pytest.mark.onlynoncluster
37+
def test_connection_pool_raises_max_connections_error():
38+
"""Test that ConnectionPool raises MaxConnectionsError and not ConnectionError"""
39+
# Use a dummy connection class that doesn't try to connect to a real Redis server
40+
pool = redis.ConnectionPool(max_connections=1, connection_class=DummyConnection)
41+
pool.get_connection()
42+
43+
with pytest.raises(redis.MaxConnectionsError):
44+
pool.get_connection()
45+
46+
47+
@pytest.mark.skipif(not hasattr(redis, "RedisCluster"), reason="RedisCluster not available")
48+
def test_cluster_handles_max_connections_error():
49+
"""
50+
Test that RedisCluster doesn't reinitialize when MaxConnectionsError is raised
51+
"""
52+
# Create a more complete mock cluster
53+
cluster = mock.MagicMock(spec=redis.RedisCluster)
54+
cluster.cluster_response_callbacks = {}
55+
cluster.RedisClusterRequestTTL = 3 # Set the TTL to avoid infinite loops
56+
cluster.nodes_manager = mock.MagicMock()
57+
node = mock.MagicMock()
58+
59+
# Mock get_redis_connection to return a mock Redis client
60+
redis_conn = mock.MagicMock()
61+
cluster.get_redis_connection.return_value = redis_conn
62+
63+
# Setup get_connection to be called and return a connection that will raise
64+
connection = mock.MagicMock()
65+
66+
# Patch the get_connection function in the cluster module
67+
with mock.patch('redis.cluster.get_connection', return_value=connection) as mock_get_conn:
68+
# Test MaxConnectionsError
69+
connection.send_command.side_effect = redis.MaxConnectionsError("Too many connections")
70+
71+
# Call the method and check that the exception is raised
72+
with pytest.raises(redis.MaxConnectionsError):
73+
redis.RedisCluster._execute_command(cluster, node, "GET", "key")
74+
75+
# Verify nodes_manager.initialize was NOT called
76+
cluster.nodes_manager.initialize.assert_not_called()
77+
78+
# Reset the mock for the next test
79+
cluster.nodes_manager.initialize.reset_mock()
80+
81+
# Now test with regular ConnectionError to ensure it DOES reinitialize
82+
connection.send_command.side_effect = redis.ConnectionError("Connection lost")
83+
84+
with pytest.raises(redis.ConnectionError):
85+
redis.RedisCluster._execute_command(cluster, node, "GET", "key")
86+
87+
# Verify nodes_manager.initialize WAS called
88+
cluster.nodes_manager.initialize.assert_called_once()

0 commit comments

Comments
 (0)