Skip to content

Commit 9a169eb

Browse files
committed
Adding default retry configuration changes for sync cluster client
1 parent fcc19d3 commit 9a169eb

File tree

3 files changed

+112
-34
lines changed

3 files changed

+112
-34
lines changed

redis/cluster.py

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from redis._parsers import CommandsParser, Encoder
1111
from redis._parsers.helpers import parse_scan
12-
from redis.backoff import default_backoff
12+
from redis.backoff import ExponentialWithJitterBackoff, NoBackoff
1313
from redis.cache import CacheConfig, CacheFactory, CacheFactoryInterface, CacheInterface
1414
from redis.client import CaseInsensitiveDict, PubSub, Redis
1515
from redis.commands import READ_COMMANDS, RedisClusterCommands
@@ -179,7 +179,7 @@ def parse_cluster_myshardid(resp, **options):
179179
"cache",
180180
"cache_config",
181181
)
182-
KWARGS_DISABLED_KEYS = ("host", "port")
182+
KWARGS_DISABLED_KEYS = ("host", "port", "retry")
183183

184184

185185
def cleanup_kwargs(**kwargs):
@@ -431,7 +431,7 @@ def replace_default_node(self, target_node: "ClusterNode" = None) -> None:
431431
# Choose a primary if the cluster contains different primaries
432432
self.nodes_manager.default_node = random.choice(primaries)
433433
else:
434-
# Otherwise, hoose a primary if the cluster contains different primaries
434+
# Otherwise, choose a primary if the cluster contains different primaries
435435
replicas = [node for node in self.get_replicas() if node != curr_node]
436436
if replicas:
437437
self.nodes_manager.default_node = random.choice(replicas)
@@ -487,6 +487,13 @@ class initializer. In the case of conflicting arguments, querystring
487487
reason="Please configure the 'load_balancing_strategy' instead",
488488
version="5.0.3",
489489
)
490+
@deprecated_args(
491+
args_to_warn=[
492+
"cluster_error_retry_attempts",
493+
],
494+
reason="Please configure the 'retry' object instead",
495+
version="6.0.0",
496+
)
490497
def __init__(
491498
self,
492499
host: Optional[str] = None,
@@ -544,9 +551,19 @@ def __init__(
544551
If you use dynamic DNS endpoints for startup nodes but CLUSTER SLOTS lists
545552
specific IP addresses, it is best to set it to false.
546553
:param cluster_error_retry_attempts:
554+
@deprecated - Please configure the 'retry' object instead
555+
In case 'retry' object is set - this argument is ignored!
556+
547557
Number of times to retry before raising an error when
548-
:class:`~.TimeoutError` or :class:`~.ConnectionError` or
558+
:class:`~.TimeoutError` or :class:`~.ConnectionError`, :class:`~.SlotNotCoveredError` or
549559
:class:`~.ClusterDownError` are encountered
560+
:param retry:
561+
A retry object that defines the retry strategy and the number of
562+
retries for the cluster client.
563+
In current implementation for the cluster client (starting form redis-py version 6.0.0)
564+
the retry object is not yet fully utilized, instead it is used just to determine
565+
the number of retries for the cluster client.
566+
In the future releases the retry object will be used to handle the cluster client retries!
550567
:param reinitialize_steps:
551568
Specifies the number of MOVED errors that need to occur before
552569
reinitializing the whole cluster topology. If a MOVED error occurs
@@ -566,7 +583,8 @@ def __init__(
566583
567584
:**kwargs:
568585
Extra arguments that will be sent into Redis instance when created
569-
(See Official redis-py doc for supported kwargs
586+
(See Official redis-py doc for supported kwargs - the only limitation
587+
is that you can't provide 'retry' object as part of kwargs.
570588
[https://github.com/andymccurdy/redis-py/blob/master/redis/client.py])
571589
Some kwargs are not supported and will raise a
572590
RedisClusterException:
@@ -581,6 +599,15 @@ def __init__(
581599
"Argument 'db' is not possible to use in cluster mode"
582600
)
583601

602+
if "retry" in kwargs:
603+
# Argument 'retry' is not possible to be used in kwargs when in cluster mode
604+
# the kwargs are set to the lower level connections to the cluster nodes
605+
# and there we provide retry configuration without retries allowed.
606+
# The retries should be handled on cluster client level.
607+
raise RedisClusterException(
608+
"Argument 'retry' is not possible to be used in kwargs when in cluster mode"
609+
)
610+
584611
# Get the startup node/s
585612
from_url = False
586613
if url is not None:
@@ -623,9 +650,11 @@ def __init__(
623650
kwargs = cleanup_kwargs(**kwargs)
624651
if retry:
625652
self.retry = retry
626-
kwargs.update({"retry": self.retry})
627653
else:
628-
kwargs.update({"retry": Retry(default_backoff(), 0)})
654+
self.retry = Retry(
655+
backoff=ExponentialWithJitterBackoff(base=1, cap=10),
656+
retries=cluster_error_retry_attempts,
657+
)
629658

630659
self.encoder = Encoder(
631660
kwargs.get("encoding", "utf-8"),
@@ -767,13 +796,13 @@ def set_default_node(self, node):
767796
self.nodes_manager.default_node = node
768797
return True
769798

770-
def get_retry(self) -> Optional["Retry"]:
799+
def get_retry(self) -> Retry:
771800
return self.retry
772801

773-
def set_retry(self, retry: "Retry") -> None:
802+
def set_retry(self, retry: Retry) -> None:
803+
if not isinstance(retry, Retry):
804+
raise TypeError("retry must be a valid instance of redis.retry.Retry")
774805
self.retry = retry
775-
for node in self.get_nodes():
776-
node.redis_connection.set_retry(retry)
777806

778807
def monitor(self, target_node=None):
779808
"""
@@ -824,6 +853,7 @@ def pipeline(self, transaction=None, shard_hint=None):
824853
read_from_replicas=self.read_from_replicas,
825854
load_balancing_strategy=self.load_balancing_strategy,
826855
reinitialize_steps=self.reinitialize_steps,
856+
retry=self.retry,
827857
lock=self._lock,
828858
)
829859

@@ -1112,9 +1142,7 @@ def _internal_execute_command(self, *args, **kwargs):
11121142
# execution since the nodes may not be valid anymore after the tables
11131143
# were reinitialized. So in case of passed target nodes,
11141144
# retry_attempts will be set to 0.
1115-
retry_attempts = (
1116-
0 if target_nodes_specified else self.cluster_error_retry_attempts
1117-
)
1145+
retry_attempts = 0 if target_nodes_specified else self.retry.get_retries()
11181146
# Add one for the first execution
11191147
execute_attempts = 1 + retry_attempts
11201148
for _ in range(execute_attempts):
@@ -1322,8 +1350,12 @@ def __eq__(self, obj):
13221350
return isinstance(obj, ClusterNode) and obj.name == self.name
13231351

13241352
def __del__(self):
1325-
if self.redis_connection is not None:
1326-
self.redis_connection.close()
1353+
try:
1354+
if self.redis_connection is not None:
1355+
self.redis_connection.close()
1356+
except Exception:
1357+
# Ignore errors when closing the connection
1358+
pass
13271359

13281360

13291361
class LoadBalancingStrategy(Enum):
@@ -1574,17 +1606,27 @@ def create_redis_connections(self, nodes):
15741606
)
15751607

15761608
def create_redis_node(self, host, port, **kwargs):
1609+
# We are configuring the connection pool to not retry
1610+
# connections on lower level clients to avoid retrying
1611+
# connections to nodes that are not reachable
1612+
# and to avoid blocking the connection pool.
1613+
# The retries will be handled on cluster client level
1614+
# where we will have proper handling of the cluster topology
1615+
node_retry_config = Retry(backoff=NoBackoff(), retries=0)
1616+
15771617
if self.from_url:
15781618
# Create a redis node with a costumed connection pool
15791619
kwargs.update({"host": host})
15801620
kwargs.update({"port": port})
15811621
kwargs.update({"cache": self._cache})
1622+
kwargs.update({"retry": node_retry_config})
15821623
r = Redis(connection_pool=self.connection_pool_class(**kwargs))
15831624
else:
15841625
r = Redis(
15851626
host=host,
15861627
port=port,
15871628
cache=self._cache,
1629+
retry=node_retry_config,
15881630
**kwargs,
15891631
)
15901632
return r
@@ -2028,6 +2070,13 @@ class ClusterPipeline(RedisCluster):
20282070
TryAgainError,
20292071
)
20302072

2073+
@deprecated_args(
2074+
args_to_warn=[
2075+
"cluster_error_retry_attempts",
2076+
],
2077+
reason="Please configure the 'retry' object instead",
2078+
version="6.0.0",
2079+
)
20312080
def __init__(
20322081
self,
20332082
nodes_manager: "NodesManager",
@@ -2039,6 +2088,7 @@ def __init__(
20392088
load_balancing_strategy: Optional[LoadBalancingStrategy] = None,
20402089
cluster_error_retry_attempts: int = 3,
20412090
reinitialize_steps: int = 5,
2091+
retry: Optional[Retry] = None,
20422092
lock=None,
20432093
**kwargs,
20442094
):
@@ -2058,6 +2108,14 @@ def __init__(
20582108
self.cluster_error_retry_attempts = cluster_error_retry_attempts
20592109
self.reinitialize_counter = 0
20602110
self.reinitialize_steps = reinitialize_steps
2111+
if retry is not None:
2112+
self.retry = retry
2113+
else:
2114+
self.retry = Retry(
2115+
backoff=ExponentialWithJitterBackoff(base=1, cap=10),
2116+
retries=self.cluster_error_retry_attempts,
2117+
)
2118+
20612119
self.encoder = Encoder(
20622120
kwargs.get("encoding", "utf-8"),
20632121
kwargs.get("encoding_errors", "strict"),
@@ -2191,7 +2249,7 @@ def send_cluster_commands(
21912249
"""
21922250
if not stack:
21932251
return []
2194-
retry_attempts = self.cluster_error_retry_attempts
2252+
retry_attempts = self.retry.get_retries()
21952253
while True:
21962254
try:
21972255
return self._send_cluster_commands(

redis/retry.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ def update_supported_errors(
4444
set(self._supported_errors + tuple(specified_errors))
4545
)
4646

47+
def get_retries(self) -> int:
48+
"""
49+
Get the number of retries.
50+
"""
51+
return self._retries
52+
53+
def update_retries(self, value: int) -> None:
54+
"""
55+
Set the number of retries.
56+
"""
57+
if not isinstance(value, int):
58+
raise ValueError("Retries count must be an integer.")
59+
self._retries = value
60+
4761
def call_with_retry(
4862
self,
4963
do: Callable[[], T],

tests/test_cluster.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
import redis
1515
from redis import Redis
1616
from redis._parsers import CommandsParser
17-
from redis.backoff import ExponentialBackoff, NoBackoff, default_backoff
17+
from redis.backoff import (
18+
ExponentialBackoff,
19+
ExponentialWithJitterBackoff,
20+
NoBackoff,
21+
)
1822
from redis.cluster import (
1923
PRIMARY,
2024
REDIS_CLUSTER_HASH_SLOTS,
@@ -884,46 +888,48 @@ def moved_redirect_effect(connection, *args, **options):
884888
def test_cluster_get_set_retry_object(self, request):
885889
retry = Retry(NoBackoff(), 2)
886890
r = _get_client(RedisCluster, request, retry=retry)
887-
assert r.get_retry()._retries == retry._retries
891+
assert r.get_retry().get_retries() == retry.get_retries()
888892
assert isinstance(r.get_retry()._backoff, NoBackoff)
889893
for node in r.get_nodes():
890-
assert node.redis_connection.get_retry()._retries == retry._retries
894+
assert node.redis_connection.get_retry().get_retries() == 0
891895
assert isinstance(node.redis_connection.get_retry()._backoff, NoBackoff)
892896
rand_node = r.get_random_node()
893897
existing_conn = rand_node.redis_connection.connection_pool.get_connection()
894898
# Change retry policy
895899
new_retry = Retry(ExponentialBackoff(), 3)
896900
r.set_retry(new_retry)
897-
assert r.get_retry()._retries == new_retry._retries
901+
assert r.get_retry().get_retries() == new_retry.get_retries()
898902
assert isinstance(r.get_retry()._backoff, ExponentialBackoff)
899903
for node in r.get_nodes():
900-
assert node.redis_connection.get_retry()._retries == new_retry._retries
901-
assert isinstance(
902-
node.redis_connection.get_retry()._backoff, ExponentialBackoff
903-
)
904-
assert existing_conn.retry._retries == new_retry._retries
904+
assert node.redis_connection.get_retry()._retries == 0
905+
assert isinstance(node.redis_connection.get_retry()._backoff, NoBackoff)
906+
assert existing_conn.retry._retries == 0
905907
new_conn = rand_node.redis_connection.connection_pool.get_connection()
906-
assert new_conn.retry._retries == new_retry._retries
908+
assert new_conn.retry._retries == 0
907909

908910
def test_cluster_retry_object(self, r) -> None:
909911
# Test default retry
910912
# FIXME: Workaround for https://github.com/redis/redis-py/issues/3030
911913
host = r.get_default_node().host
912914

913-
retry = r.get_connection_kwargs().get("retry")
915+
# test default retry config
916+
retry = r.get_retry()
914917
assert isinstance(retry, Retry)
915-
assert retry._retries == 0
916-
assert isinstance(retry._backoff, type(default_backoff()))
918+
assert retry.get_retries() == 3
919+
assert isinstance(retry._backoff, type(ExponentialWithJitterBackoff()))
917920
node1 = r.get_node(host, 16379).redis_connection
918921
node2 = r.get_node(host, 16380).redis_connection
919-
assert node1.get_retry()._retries == node2.get_retry()._retries
922+
assert node1.get_retry()._retries == 0
923+
assert node2.get_retry()._retries == 0
920924

921-
# Test custom retry
925+
# Test custom retry is not applied to nodes
922926
retry = Retry(ExponentialBackoff(10, 5), 5)
923927
rc_custom_retry = RedisCluster(host, 16379, retry=retry)
924928
assert (
925-
rc_custom_retry.get_node(host, 16379).redis_connection.get_retry()._retries
926-
== retry._retries
929+
rc_custom_retry.get_node(host, 16379)
930+
.redis_connection.get_retry()
931+
.get_retries()
932+
== 0
927933
)
928934

929935
def test_replace_cluster_node(self, r) -> None:

0 commit comments

Comments
 (0)