Skip to content

Commit 0392576

Browse files
authored
Added retry mechanism on socket timeouts when connecting to the server (#1895)
1 parent fa5841e commit 0392576

File tree

3 files changed

+58
-4
lines changed

3 files changed

+58
-4
lines changed

redis/connection.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,9 @@ def connect(self):
604604
if self._sock:
605605
return
606606
try:
607-
sock = self._connect()
607+
sock = self.retry.call_with_retry(
608+
lambda: self._connect(), lambda error: self.disconnect(error)
609+
)
608610
except socket.timeout:
609611
raise TimeoutError("Timeout connecting to server")
610612
except OSError as e:
@@ -721,7 +723,7 @@ def on_connect(self):
721723
if str_if_bytes(self.read_response()) != "OK":
722724
raise ConnectionError("Invalid Database")
723725

724-
def disconnect(self):
726+
def disconnect(self, *args):
725727
"Disconnects from the Redis server"
726728
self._parser.on_disconnect()
727729
if self._sock is None:

redis/retry.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import socket
12
from time import sleep
23

34
from redis.exceptions import ConnectionError, TimeoutError
@@ -7,7 +8,10 @@ class Retry:
78
"""Retry a specific number of times after a failure"""
89

910
def __init__(
10-
self, backoff, retries, supported_errors=(ConnectionError, TimeoutError)
11+
self,
12+
backoff,
13+
retries,
14+
supported_errors=(ConnectionError, TimeoutError, socket.timeout),
1115
):
1216
"""
1317
Initialize a `Retry` object with a `Backoff` object

tests/test_connection.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import socket
12
import types
23
from unittest import mock
4+
from unittest.mock import patch
35

46
import pytest
57

8+
from redis.backoff import NoBackoff
69
from redis.connection import Connection
7-
from redis.exceptions import InvalidResponse
10+
from redis.exceptions import ConnectionError, InvalidResponse, TimeoutError
11+
from redis.retry import Retry
812
from redis.utils import HIREDIS_AVAILABLE
913

1014
from .conftest import skip_if_server_version_lt
@@ -74,3 +78,47 @@ def test_disconnect__close_OSError(self):
7478
mock_sock.shutdown.assert_called_once()
7579
mock_sock.close.assert_called_once()
7680
assert conn._sock is None
81+
82+
def clear(self, conn):
83+
conn.retry_on_error.clear()
84+
85+
def test_retry_connect_on_timeout_error(self):
86+
"""Test that the _connect function is retried in case of a timeout"""
87+
conn = Connection(retry_on_timeout=True, retry=Retry(NoBackoff(), 3))
88+
origin_connect = conn._connect
89+
conn._connect = mock.Mock()
90+
91+
def mock_connect():
92+
# connect only on the last retry
93+
if conn._connect.call_count <= 2:
94+
raise socket.timeout
95+
else:
96+
return origin_connect()
97+
98+
conn._connect.side_effect = mock_connect
99+
conn.connect()
100+
assert conn._connect.call_count == 3
101+
self.clear(conn)
102+
103+
def test_connect_without_retry_on_os_error(self):
104+
"""Test that the _connect function is not being retried in case of a OSError"""
105+
with patch.object(Connection, "_connect") as _connect:
106+
_connect.side_effect = OSError("")
107+
conn = Connection(retry_on_timeout=True, retry=Retry(NoBackoff(), 2))
108+
with pytest.raises(ConnectionError):
109+
conn.connect()
110+
assert _connect.call_count == 1
111+
self.clear(conn)
112+
113+
def test_connect_timeout_error_without_retry(self):
114+
"""Test that the _connect function is not being retried if retry_on_timeout is
115+
set to False"""
116+
conn = Connection(retry_on_timeout=False)
117+
conn._connect = mock.Mock()
118+
conn._connect.side_effect = socket.timeout
119+
120+
with pytest.raises(TimeoutError) as e:
121+
conn.connect()
122+
assert conn._connect.call_count == 1
123+
assert str(e.value) == "Timeout connecting to server"
124+
self.clear(conn)

0 commit comments

Comments
 (0)