Skip to content

Commit a630b39

Browse files
petyaslavovaManelCoutinhoSensei
authored andcommitted
Updating default retry strategy for standalone clients. 3 retries with ExponentialWithJitterBackoff become the default config. (redis#3614)
* Changing the default retry configuration for Redis standalone clients. * Updating default retry strategy for standalone clients. 3 retries with ExponentialWithJitterBackoff become the default config. * Applying review comments - removing unused methods from retry objects, updating pydocs of error handler method
1 parent 2b6e184 commit a630b39

File tree

6 files changed

+165
-142
lines changed

6 files changed

+165
-142
lines changed

redis/asyncio/client.py

Lines changed: 74 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
)
4040
from redis.asyncio.lock import Lock
4141
from redis.asyncio.retry import Retry
42+
from redis.backoff import ExponentialWithJitterBackoff
4243
from redis.client import (
4344
EMPTY_RESPONSE,
4445
NEVER_DECODE,
@@ -65,14 +66,14 @@
6566
PubSubError,
6667
RedisError,
6768
ResponseError,
68-
TimeoutError,
6969
WatchError,
7070
)
7171
from redis.typing import ChannelT, EncodableT, KeyT
7272
from redis.utils import (
7373
HIREDIS_AVAILABLE,
7474
SSL_AVAILABLE,
7575
_set_info_logger,
76+
deprecated_args,
7677
deprecated_function,
7778
get_lib_version,
7879
safe_str,
@@ -208,6 +209,11 @@ def from_pool(
208209
client.auto_close_connection_pool = True
209210
return client
210211

212+
@deprecated_args(
213+
args_to_warn=["retry_on_timeout"],
214+
reason="TimeoutError is included by default.",
215+
version="6.0.0",
216+
)
211217
def __init__(
212218
self,
213219
*,
@@ -226,6 +232,9 @@ def __init__(
226232
decode_responses: bool = False,
227233
check_server_ready: bool = False,
228234
retry_on_timeout: bool = False,
235+
retry: Retry = Retry(
236+
backoff=ExponentialWithJitterBackoff(base=1, cap=10), retries=3
237+
),
229238
retry_on_error: Optional[list] = None,
230239
ssl: bool = False,
231240
ssl_keyfile: Optional[str] = None,
@@ -243,7 +252,6 @@ def __init__(
243252
lib_name: Optional[str] = "redis-py",
244253
lib_version: Optional[str] = get_lib_version(),
245254
username: Optional[str] = None,
246-
retry: Optional[Retry] = None,
247255
auto_close_connection_pool: Optional[bool] = None,
248256
redis_connect_func=None,
249257
credential_provider: Optional[CredentialProvider] = None,
@@ -252,10 +260,24 @@ def __init__(
252260
):
253261
"""
254262
Initialize a new Redis client.
255-
To specify a retry policy for specific errors, first set
256-
`retry_on_error` to a list of the error/s to retry on, then set
257-
`retry` to a valid `Retry` object.
258-
To retry on TimeoutError, `retry_on_timeout` can also be set to `True`.
263+
264+
To specify a retry policy for specific errors, you have two options:
265+
266+
1. Set the `retry_on_error` to a list of the error/s to retry on, and
267+
you can also set `retry` to a valid `Retry` object(in case the default
268+
one is not appropriate) - with this approach the retries will be triggered
269+
on the default errors specified in the Retry object enriched with the
270+
errors specified in `retry_on_error`.
271+
272+
2. Define a `Retry` object with configured 'supported_errors' and set
273+
it to the `retry` parameter - with this approach you completely redefine
274+
the errors on which retries will happen.
275+
276+
`retry_on_timeout` is deprecated - please include the TimeoutError
277+
either in the Retry object or in the `retry_on_error` list.
278+
279+
When 'connection_pool' is provided - the retry configuration of the
280+
provided pool will be used.
259281
260282
Args:
261283
check_server_ready: if `True`, an extra handshake is performed by sending a PING command, since
@@ -285,8 +307,6 @@ def __init__(
285307
# Create internal connection pool, expected to be closed by Redis instance
286308
if not retry_on_error:
287309
retry_on_error = []
288-
if retry_on_timeout is True:
289-
retry_on_error.append(TimeoutError)
290310
kwargs = {
291311
"db": db,
292312
"username": username,
@@ -297,7 +317,6 @@ def __init__(
297317
"encoding_errors": encoding_errors,
298318
"decode_responses": decode_responses,
299319
"check_server_ready": check_server_ready,
300-
"retry_on_timeout": retry_on_timeout,
301320
"retry_on_error": retry_on_error,
302321
"retry": copy.deepcopy(retry),
303322
"max_connections": max_connections,
@@ -409,10 +428,10 @@ def get_connection_kwargs(self):
409428
"""Get the connection's key-word arguments"""
410429
return self.connection_pool.connection_kwargs
411430

412-
def get_retry(self) -> Optional["Retry"]:
431+
def get_retry(self) -> Optional[Retry]:
413432
return self.get_connection_kwargs().get("retry")
414433

415-
def set_retry(self, retry: "Retry") -> None:
434+
def set_retry(self, retry: Retry) -> None:
416435
self.get_connection_kwargs().update({"retry": retry})
417436
self.connection_pool.set_retry(retry)
418437

@@ -639,18 +658,17 @@ async def _send_command_parse_response(self, conn, command_name, *args, **option
639658
await conn.send_command(*args)
640659
return await self.parse_response(conn, command_name, **options)
641660

642-
async def _disconnect_raise(self, conn: Connection, error: Exception):
661+
async def _close_connection(self, conn: Connection):
643662
"""
644-
Close the connection and raise an exception
645-
if retry_on_error is not set or the error
646-
is not one of the specified error types
663+
Close the connection before retrying.
664+
665+
The supported exceptions are already checked in the
666+
retry object so we don't need to do it here.
667+
668+
After we disconnect the connection, it will try to reconnect and
669+
do a health check as part of the send_command logic(on connection level).
647670
"""
648671
await conn.disconnect()
649-
if (
650-
conn.retry_on_error is None
651-
or isinstance(error, tuple(conn.retry_on_error)) is False
652-
):
653-
raise error
654672

655673
# COMMAND EXECUTION AND PROTOCOL PARSING
656674
async def execute_command(self, *args, **options):
@@ -667,7 +685,7 @@ async def execute_command(self, *args, **options):
667685
lambda: self._send_command_parse_response(
668686
conn, command_name, *args, **options
669687
),
670-
lambda error: self._disconnect_raise(conn, error),
688+
lambda _: self._close_connection(conn),
671689
)
672690
finally:
673691
if self.single_connection_client:
@@ -935,19 +953,11 @@ async def connect(self):
935953
)
936954
)
937955

938-
async def _disconnect_raise_connect(self, conn, error):
956+
async def _reconnect(self, conn):
939957
"""
940-
Close the connection and raise an exception
941-
if retry_on_error is not set or the error is not one
942-
of the specified error types. Otherwise, try to
943-
reconnect
958+
Try to reconnect
944959
"""
945960
await conn.disconnect()
946-
if (
947-
conn.retry_on_error is None
948-
or isinstance(error, tuple(conn.retry_on_error)) is False
949-
):
950-
raise error
951961
await conn.connect()
952962

953963
async def _execute(self, conn, command, *args, **kwargs):
@@ -960,7 +970,7 @@ async def _execute(self, conn, command, *args, **kwargs):
960970
"""
961971
return await conn.retry.call_with_retry(
962972
lambda: command(*args, **kwargs),
963-
lambda error: self._disconnect_raise_connect(conn, error),
973+
lambda _: self._reconnect(conn),
964974
)
965975

966976
async def parse_response(self, block: bool = True, timeout: float = 0):
@@ -1251,7 +1261,8 @@ class Pipeline(Redis): # lgtm [py/init-calls-subclass]
12511261
in one transmission. This is convenient for batch processing, such as
12521262
saving all the values in a list to Redis.
12531263
1254-
All commands executed within a pipeline are wrapped with MULTI and EXEC
1264+
All commands executed within a pipeline(when running in transactional mode,
1265+
which is the default behavior) are wrapped with MULTI and EXEC
12551266
calls. This guarantees all commands executed in the pipeline will be
12561267
executed atomically.
12571268
@@ -1280,7 +1291,7 @@ def __init__(
12801291
self.shard_hint = shard_hint
12811292
self.watching = False
12821293
self.command_stack: CommandStackT = []
1283-
self.scripts: Set["Script"] = set()
1294+
self.scripts: Set[Script] = set()
12841295
self.explicit_transaction = False
12851296

12861297
async def __aenter__(self: _RedisT) -> _RedisT:
@@ -1352,36 +1363,36 @@ def execute_command(
13521363
return self.immediate_execute_command(*args, **kwargs)
13531364
return self.pipeline_execute_command(*args, **kwargs)
13541365

1355-
async def _disconnect_reset_raise(self, conn, error):
1366+
async def _disconnect_reset_raise_on_watching(
1367+
self,
1368+
conn: Connection,
1369+
error: Exception,
1370+
):
13561371
"""
1357-
Close the connection, reset watching state and
1358-
raise an exception if we were watching,
1359-
if retry_on_error is not set or the error is not one
1360-
of the specified error types.
1372+
Close the connection reset watching state and
1373+
raise an exception if we were watching.
1374+
1375+
The supported exceptions are already checked in the
1376+
retry object so we don't need to do it here.
1377+
1378+
After we disconnect the connection, it will try to reconnect and
1379+
do a health check as part of the send_command logic(on connection level).
13611380
"""
13621381
await conn.disconnect()
13631382
# if we were already watching a variable, the watch is no longer
13641383
# valid since this connection has died. raise a WatchError, which
13651384
# indicates the user should retry this transaction.
13661385
if self.watching:
1367-
await self.aclose()
1386+
await self.reset()
13681387
raise WatchError(
1369-
"A ConnectionError occurred on while watching one or more keys"
1388+
f"A {type(error).__name__} occurred while watching one or more keys"
13701389
)
1371-
# if retry_on_error is not set or the error is not one
1372-
# of the specified error types, raise it
1373-
if (
1374-
conn.retry_on_error is None
1375-
or isinstance(error, tuple(conn.retry_on_error)) is False
1376-
):
1377-
await self.aclose()
1378-
raise
13791390

13801391
async def immediate_execute_command(self, *args, **options):
13811392
"""
1382-
Execute a command immediately, but don't auto-retry on a
1383-
ConnectionError if we're already WATCHing a variable. Used when
1384-
issuing WATCH or subsequent commands retrieving their values but before
1393+
Execute a command immediately, but don't auto-retry on the supported
1394+
errors for retry if we're already WATCHing a variable.
1395+
Used when issuing WATCH or subsequent commands retrieving their values but before
13851396
MULTI is called.
13861397
"""
13871398
command_name = args[0]
@@ -1395,7 +1406,7 @@ async def immediate_execute_command(self, *args, **options):
13951406
lambda: self._send_command_parse_response(
13961407
conn, command_name, *args, **options
13971408
),
1398-
lambda error: self._disconnect_reset_raise(conn, error),
1409+
lambda error: self._disconnect_reset_raise_on_watching(conn, error),
13991410
)
14001411

14011412
def pipeline_execute_command(self, *args, **options):
@@ -1550,28 +1561,24 @@ async def load_scripts(self):
15501561
if not exist:
15511562
s.sha = await immediate("SCRIPT LOAD", s.script)
15521563

1553-
async def _disconnect_raise_reset(self, conn: Connection, error: Exception):
1564+
async def _disconnect_raise_on_watching(self, conn: Connection, error: Exception):
15541565
"""
1555-
Close the connection, raise an exception if we were watching,
1556-
and raise an exception if retry_on_error is not set or the
1557-
error is not one of the specified error types.
1566+
Close the connection, raise an exception if we were watching.
1567+
1568+
The supported exceptions are already checked in the
1569+
retry object so we don't need to do it here.
1570+
1571+
After we disconnect the connection, it will try to reconnect and
1572+
do a health check as part of the send_command logic(on connection level).
15581573
"""
15591574
await conn.disconnect()
15601575
# if we were watching a variable, the watch is no longer valid
15611576
# since this connection has died. raise a WatchError, which
15621577
# indicates the user should retry this transaction.
15631578
if self.watching:
15641579
raise WatchError(
1565-
"A ConnectionError occurred on while watching one or more keys"
1580+
f"A {type(error).__name__} occurred while watching one or more keys"
15661581
)
1567-
# if retry_on_error is not set or the error is not one
1568-
# of the specified error types, raise it
1569-
if (
1570-
conn.retry_on_error is None
1571-
or isinstance(error, tuple(conn.retry_on_error)) is False
1572-
):
1573-
await self.reset()
1574-
raise
15751582

15761583
async def execute(self, raise_on_error: bool = True) -> List[Any]:
15771584
"""Execute all the commands in the current pipeline"""
@@ -1596,7 +1603,7 @@ async def execute(self, raise_on_error: bool = True) -> List[Any]:
15961603
try:
15971604
return await conn.retry.call_with_retry(
15981605
lambda: execute(conn, stack, raise_on_error),
1599-
lambda error: self._disconnect_raise_reset(conn, error),
1606+
lambda error: self._disconnect_raise_on_watching(conn, error),
16001607
)
16011608
finally:
16021609
await self.reset()

0 commit comments

Comments
 (0)