Repository: redis/redis-py

Issue:
Question: resolving hostname when retry on connection timeout
I wonder if Redis client resolve again the hostname if connection timeout occurred while setting `retry_on_timeout=True`.
I'm asking since sometime the mapping between hostname to ip can be invalid since the DNS changed the mapping.


Discussion:
Yes,
By setting retry_on_timeout=True, when timeoutError is thrown, the client disconnects its current connection and creates a new one. Upon creating a new connection, the hostname is resolved again.

To retry on other errors too, you can use the new argument `retry_on_error` and pass other error types in a list.

@barshaul thanks for your replay.
Another question - how many retries and what is the delay between each retry?
If you don't pass a Retry instance using the client's 'retry' argument, it will create a default one for you. The default Retry object has 1 retries and NoBackoff() strategy.

Here's more info about the Retry / Backoff strategies: 

- Backoff base class has four backoff strategies based on https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/. see ConstantBackoff, NoBackoff, ExponentialBackoff, FullJitterBackoff.
- Retry class is constructed from a backoff strategy and a maximum number of retries

Example how to create a client with retry object:

```
from redis.backoff import ExponentialBackoff
from redis.retry import Retry
from redis.client import Redis
from redis.exceptions import (
    BusyLoadingError,
    ConnectionError,
    TimeoutError
)

retry = Retry(ExponentialBackoff(), 3) // Run 3 retries with exponential backoff strategy 
r = Redis(host='localhost', port=6379, retry=retry, retry_on_error=[BusyLoadingError, ConnectionError, TimeoutError])
r_only_timeout = Redis(host='localhost', port=6379, retry=retry, retry_on_timeout=True)
```

@barshaul Thank you!
@barshaul I tried the solution you suggested and it's not working when trying to connect to the server.
by reading the code I saw that the retry mechanism only apply at the command execution level and not when trying to connect to the server.

any suggestion?

Resolving patch:
diff --git a/redis/connection.py b/redis/connection.py
--- a/redis/connection.py
+++ b/redis/connection.py
@@ -604,7 +604,9 @@ def connect(self):
         if self._sock:
             return
         try:
-            sock = self._connect()
+            sock = self.retry.call_with_retry(
+                lambda: self._connect(), lambda error: self.disconnect(error)
+            )
         except socket.timeout:
             raise TimeoutError("Timeout connecting to server")
         except OSError as e:
@@ -721,7 +723,7 @@ def on_connect(self):
             if str_if_bytes(self.read_response()) != "OK":
                 raise ConnectionError("Invalid Database")
 
-    def disconnect(self):
+    def disconnect(self, *args):
         "Disconnects from the Redis server"
         self._parser.on_disconnect()
         if self._sock is None:
diff --git a/redis/retry.py b/redis/retry.py
--- a/redis/retry.py
+++ b/redis/retry.py
@@ -1,3 +1,4 @@
+import socket
 from time import sleep
 
 from redis.exceptions import ConnectionError, TimeoutError
@@ -7,7 +8,10 @@ class Retry:
     """Retry a specific number of times after a failure"""
 
     def __init__(
-        self, backoff, retries, supported_errors=(ConnectionError, TimeoutError)
+        self,
+        backoff,
+        retries,
+        supported_errors=(ConnectionError, TimeoutError, socket.timeout),
     ):
         """
         Initialize a `Retry` object with a `Backoff` object


Test patch:
diff --git a/tests/test_connection.py b/tests/test_connection.py
--- a/tests/test_connection.py
+++ b/tests/test_connection.py
@@ -1,10 +1,14 @@
+import socket
 import types
 from unittest import mock
+from unittest.mock import patch
 
 import pytest
 
+from redis.backoff import NoBackoff
 from redis.connection import Connection
-from redis.exceptions import InvalidResponse
+from redis.exceptions import ConnectionError, InvalidResponse, TimeoutError
+from redis.retry import Retry
 from redis.utils import HIREDIS_AVAILABLE
 
 from .conftest import skip_if_server_version_lt
@@ -74,3 +78,47 @@ def test_disconnect__close_OSError(self):
         mock_sock.shutdown.assert_called_once()
         mock_sock.close.assert_called_once()
         assert conn._sock is None
+
+    def clear(self, conn):
+        conn.retry_on_error.clear()
+
+    def test_retry_connect_on_timeout_error(self):
+        """Test that the _connect function is retried in case of a timeout"""
+        conn = Connection(retry_on_timeout=True, retry=Retry(NoBackoff(), 3))
+        origin_connect = conn._connect
+        conn._connect = mock.Mock()
+
+        def mock_connect():
+            # connect only on the last retry
+            if conn._connect.call_count <= 2:
+                raise socket.timeout
+            else:
+                return origin_connect()
+
+        conn._connect.side_effect = mock_connect
+        conn.connect()
+        assert conn._connect.call_count == 3
+        self.clear(conn)
+
+    def test_connect_without_retry_on_os_error(self):
+        """Test that the _connect function is not being retried in case of a OSError"""
+        with patch.object(Connection, "_connect") as _connect:
+            _connect.side_effect = OSError("")
+            conn = Connection(retry_on_timeout=True, retry=Retry(NoBackoff(), 2))
+            with pytest.raises(ConnectionError):
+                conn.connect()
+            assert _connect.call_count == 1
+            self.clear(conn)
+
+    def test_connect_timeout_error_without_retry(self):
+        """Test that the _connect function is not being retried if retry_on_timeout is
+        set to False"""
+        conn = Connection(retry_on_timeout=False)
+        conn._connect = mock.Mock()
+        conn._connect.side_effect = socket.timeout
+
+        with pytest.raises(TimeoutError) as e:
+            conn.connect()
+        assert conn._connect.call_count == 1
+        assert str(e.value) == "Timeout connecting to server"
+        self.clear(conn)