Skip to content

Commit 92dd9b0

Browse files
committed
Adding default retry configuration changes for sync cluster client
1 parent a4df6b2 commit 92dd9b0

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):
@@ -436,7 +436,7 @@ def replace_default_node(self, target_node: "ClusterNode" = None) -> None:
436436
# Choose a primary if the cluster contains different primaries
437437
self.nodes_manager.default_node = random.choice(primaries)
438438
else:
439-
# Otherwise, hoose a primary if the cluster contains different primaries
439+
# Otherwise, choose a primary if the cluster contains different primaries
440440
replicas = [node for node in self.get_replicas() if node != curr_node]
441441
if replicas:
442442
self.nodes_manager.default_node = random.choice(replicas)
@@ -492,6 +492,13 @@ class initializer. In the case of conflicting arguments, querystring
492492
reason="Please configure the 'load_balancing_strategy' instead",
493493
version="5.0.3",
494494
)
495+
@deprecated_args(
496+
args_to_warn=[
497+
"cluster_error_retry_attempts",
498+
],
499+
reason="Please configure the 'retry' object instead",
500+
version="6.0.0",
501+
)
495502
def __init__(
496503
self,
497504
host: Optional[str] = None,
@@ -549,9 +556,19 @@ def __init__(
549556
If you use dynamic DNS endpoints for startup nodes but CLUSTER SLOTS lists
550557
specific IP addresses, it is best to set it to false.
551558
:param cluster_error_retry_attempts:
559+
@deprecated - Please configure the 'retry' object instead
560+
In case 'retry' object is set - this argument is ignored!
561+
552562
Number of times to retry before raising an error when
553-
:class:`~.TimeoutError` or :class:`~.ConnectionError` or
563+
:class:`~.TimeoutError` or :class:`~.ConnectionError`, :class:`~.SlotNotCoveredError` or
554564
:class:`~.ClusterDownError` are encountered
565+
:param retry:
566+
A retry object that defines the retry strategy and the number of
567+
retries for the cluster client.
568+
In current implementation for the cluster client (starting form redis-py version 6.0.0)
569+
the retry object is not yet fully utilized, instead it is used just to determine
570+
the number of retries for the cluster client.
571+
In the future releases the retry object will be used to handle the cluster client retries!
555572
:param reinitialize_steps:
556573
Specifies the number of MOVED errors that need to occur before
557574
reinitializing the whole cluster topology. If a MOVED error occurs
@@ -571,7 +588,8 @@ def __init__(
571588
572589
:**kwargs:
573590
Extra arguments that will be sent into Redis instance when created
574-
(See Official redis-py doc for supported kwargs
591+
(See Official redis-py doc for supported kwargs - the only limitation
592+
is that you can't provide 'retry' object as part of kwargs.
575593
[https://github.com/andymccurdy/redis-py/blob/master/redis/client.py])
576594
Some kwargs are not supported and will raise a
577595
RedisClusterException:
@@ -586,6 +604,15 @@ def __init__(
586604
"Argument 'db' is not possible to use in cluster mode"
587605
)
588606

607+
if "retry" in kwargs:
608+
# Argument 'retry' is not possible to be used in kwargs when in cluster mode
609+
# the kwargs are set to the lower level connections to the cluster nodes
610+
# and there we provide retry configuration without retries allowed.
611+
# The retries should be handled on cluster client level.
612+
raise RedisClusterException(
613+
"Argument 'retry' is not possible to be used in kwargs when in cluster mode"
614+
)
615+
589616
# Get the startup node/s
590617
from_url = False
591618
if url is not None:
@@ -628,9 +655,11 @@ def __init__(
628655
kwargs = cleanup_kwargs(**kwargs)
629656
if retry:
630657
self.retry = retry
631-
kwargs.update({"retry": self.retry})
632658
else:
633-
kwargs.update({"retry": Retry(default_backoff(), 0)})
659+
self.retry = Retry(
660+
backoff=ExponentialWithJitterBackoff(base=1, cap=10),
661+
retries=cluster_error_retry_attempts,
662+
)
634663

635664
self.encoder = Encoder(
636665
kwargs.get("encoding", "utf-8"),
@@ -772,13 +801,13 @@ def set_default_node(self, node):
772801
self.nodes_manager.default_node = node
773802
return True
774803

775-
def get_retry(self) -> Optional["Retry"]:
804+
def get_retry(self) -> Retry:
776805
return self.retry
777806

778-
def set_retry(self, retry: "Retry") -> None:
807+
def set_retry(self, retry: Retry) -> None:
808+
if not isinstance(retry, Retry):
809+
raise TypeError("retry must be a valid instance of redis.retry.Retry")
779810
self.retry = retry
780-
for node in self.get_nodes():
781-
node.redis_connection.set_retry(retry)
782811

783812
def monitor(self, target_node=None):
784813
"""
@@ -829,6 +858,7 @@ def pipeline(self, transaction=None, shard_hint=None):
829858
read_from_replicas=self.read_from_replicas,
830859
load_balancing_strategy=self.load_balancing_strategy,
831860
reinitialize_steps=self.reinitialize_steps,
861+
retry=self.retry,
832862
lock=self._lock,
833863
)
834864

@@ -1117,9 +1147,7 @@ def _internal_execute_command(self, *args, **kwargs):
11171147
# execution since the nodes may not be valid anymore after the tables
11181148
# were reinitialized. So in case of passed target nodes,
11191149
# retry_attempts will be set to 0.
1120-
retry_attempts = (
1121-
0 if target_nodes_specified else self.cluster_error_retry_attempts
1122-
)
1150+
retry_attempts = 0 if target_nodes_specified else self.retry.get_retries()
11231151
# Add one for the first execution
11241152
execute_attempts = 1 + retry_attempts
11251153
for _ in range(execute_attempts):
@@ -1333,8 +1361,12 @@ def __eq__(self, obj):
13331361
return isinstance(obj, ClusterNode) and obj.name == self.name
13341362

13351363
def __del__(self):
1336-
if self.redis_connection is not None:
1337-
self.redis_connection.close()
1364+
try:
1365+
if self.redis_connection is not None:
1366+
self.redis_connection.close()
1367+
except Exception:
1368+
# Ignore errors when closing the connection
1369+
pass
13381370

13391371

13401372
class LoadBalancingStrategy(Enum):
@@ -1585,17 +1617,27 @@ def create_redis_connections(self, nodes):
15851617
)
15861618

15871619
def create_redis_node(self, host, port, **kwargs):
1620+
# We are configuring the connection pool to not retry
1621+
# connections on lower level clients to avoid retrying
1622+
# connections to nodes that are not reachable
1623+
# and to avoid blocking the connection pool.
1624+
# The retries will be handled on cluster client level
1625+
# where we will have proper handling of the cluster topology
1626+
node_retry_config = Retry(backoff=NoBackoff(), retries=0)
1627+
15881628
if self.from_url:
15891629
# Create a redis node with a costumed connection pool
15901630
kwargs.update({"host": host})
15911631
kwargs.update({"port": port})
15921632
kwargs.update({"cache": self._cache})
1633+
kwargs.update({"retry": node_retry_config})
15931634
r = Redis(connection_pool=self.connection_pool_class(**kwargs))
15941635
else:
15951636
r = Redis(
15961637
host=host,
15971638
port=port,
15981639
cache=self._cache,
1640+
retry=node_retry_config,
15991641
**kwargs,
16001642
)
16011643
return r
@@ -2039,6 +2081,13 @@ class ClusterPipeline(RedisCluster):
20392081
TryAgainError,
20402082
)
20412083

2084+
@deprecated_args(
2085+
args_to_warn=[
2086+
"cluster_error_retry_attempts",
2087+
],
2088+
reason="Please configure the 'retry' object instead",
2089+
version="6.0.0",
2090+
)
20422091
def __init__(
20432092
self,
20442093
nodes_manager: "NodesManager",
@@ -2050,6 +2099,7 @@ def __init__(
20502099
load_balancing_strategy: Optional[LoadBalancingStrategy] = None,
20512100
cluster_error_retry_attempts: int = 3,
20522101
reinitialize_steps: int = 5,
2102+
retry: Optional[Retry] = None,
20532103
lock=None,
20542104
**kwargs,
20552105
):
@@ -2069,6 +2119,14 @@ def __init__(
20692119
self.cluster_error_retry_attempts = cluster_error_retry_attempts
20702120
self.reinitialize_counter = 0
20712121
self.reinitialize_steps = reinitialize_steps
2122+
if retry is not None:
2123+
self.retry = retry
2124+
else:
2125+
self.retry = Retry(
2126+
backoff=ExponentialWithJitterBackoff(base=1, cap=10),
2127+
retries=self.cluster_error_retry_attempts,
2128+
)
2129+
20722130
self.encoder = Encoder(
20732131
kwargs.get("encoding", "utf-8"),
20742132
kwargs.get("encoding_errors", "strict"),
@@ -2202,7 +2260,7 @@ def send_cluster_commands(
22022260
"""
22032261
if not stack:
22042262
return []
2205-
retry_attempts = self.cluster_error_retry_attempts
2263+
retry_attempts = self.retry.get_retries()
22062264
while True:
22072265
try:
22082266
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)