Repository: googleapis/google-resumable-media-python

Issue:
Add support for known network issues to be retryable
Add support for known network issues to be retryable and align behavior across Storage SDKs.

Such errors as below are categorized as known network/transfer errors in gsutil and gcloud, see internal doc go/python-storage-retry-known-errors for more details

|Known Network Errors(Retryable)|python storage clients|gcloud|gsutil|
|:----|:----|:----|:----|
|urllib3.exceptions.ProtocolError|Y|Y|Y|
|urllib3.exceptions.SSLError|N (need support)|Y|Y|
|urllib3.exceptions.TimeoutError|N (need support)|Y|Y|
|requests.exceptions.SSLError|Y|Y|Y|


Discussion:

Patch:
diff --git a/google/resumable_media/requests/_request_helpers.py b/google/resumable_media/requests/_request_helpers.py
index fbeda5a..051f0ba 100644
--- a/google/resumable_media/requests/_request_helpers.py
+++ b/google/resumable_media/requests/_request_helpers.py
@@ -17,6 +17,7 @@
 This utilities are explicitly catered to ``requests``-like transports.
 """
 
+import http.client
 import requests.exceptions
 import urllib3.exceptions  # type: ignore
 
@@ -35,10 +36,16 @@ _DEFAULT_CONNECT_TIMEOUT = 61
 _DEFAULT_READ_TIMEOUT = 60
 
 _CONNECTION_ERROR_CLASSES = (
+    http.client.BadStatusLine,
+    http.client.IncompleteRead,
+    http.client.ResponseNotReady,
     requests.exceptions.ConnectionError,
     requests.exceptions.ChunkedEncodingError,
     requests.exceptions.Timeout,
+    urllib3.exceptions.PoolError,
     urllib3.exceptions.ProtocolError,
+    urllib3.exceptions.SSLError,
+    urllib3.exceptions.TimeoutError,
     ConnectionError,  # Python 3.x only, superclass of ConnectionResetError.
 )
 


Test patch:
diff --git a/tests/unit/requests/test__helpers.py b/tests/unit/requests/test__helpers.py
index dc940f2..de85991 100644
--- a/tests/unit/requests/test__helpers.py
+++ b/tests/unit/requests/test__helpers.py
@@ -165,14 +165,18 @@ class Test_wait_and_retry(object):
 
     @mock.patch("time.sleep")
     @mock.patch("random.randint")
-    def test_success_with_retry_connection_error(self, randint_mock, sleep_mock):
-        randint_mock.side_effect = [125, 625, 375]
+    def test_retry_success_http_standard_lib_connection_errors(
+        self, randint_mock, sleep_mock
+    ):
+        randint_mock.side_effect = [125, 625, 500, 875, 375]
 
-        response = _make_response(http.client.NOT_FOUND)
+        status_code = int(http.client.OK)
+        response = _make_response(status_code)
         responses = [
-            ConnectionResetError,  # Subclass of ConnectionError
-            urllib3.exceptions.ConnectionError,
-            requests.exceptions.ConnectionError,
+            http.client.BadStatusLine(""),
+            http.client.IncompleteRead(""),
+            http.client.ResponseNotReady,
+            ConnectionError,
             response,
         ]
         func = mock.Mock(side_effect=responses, spec=[])
@@ -183,28 +187,29 @@ class Test_wait_and_retry(object):
         )
 
         assert ret_val == responses[-1]
-
-        assert func.call_count == 4
-        assert func.mock_calls == [mock.call()] * 4
-
-        assert randint_mock.call_count == 3
-        assert randint_mock.mock_calls == [mock.call(0, 1000)] * 3
-
-        assert sleep_mock.call_count == 3
+        assert func.call_count == 5
+        assert func.mock_calls == [mock.call()] * 5
+        assert randint_mock.call_count == 4
+        assert randint_mock.mock_calls == [mock.call(0, 1000)] * 4
+        assert sleep_mock.call_count == 4
         sleep_mock.assert_any_call(1.125)
         sleep_mock.assert_any_call(2.625)
-        sleep_mock.assert_any_call(4.375)
+        sleep_mock.assert_any_call(4.500)
+        sleep_mock.assert_any_call(8.875)
 
     @mock.patch("time.sleep")
     @mock.patch("random.randint")
-    def test_success_with_retry_chunked_encoding_error(self, randint_mock, sleep_mock):
-        randint_mock.side_effect = [125, 625, 375]
+    def test_retry_success_requests_lib_connection_errors(
+        self, randint_mock, sleep_mock
+    ):
+        randint_mock.side_effect = [125, 625, 500, 875]
 
         status_code = int(http.client.OK)
         response = _make_response(status_code)
         responses = [
+            requests.exceptions.ConnectionError,
             requests.exceptions.ChunkedEncodingError,
-            requests.exceptions.ChunkedEncodingError,
+            requests.exceptions.Timeout,
             response,
         ]
         func = mock.Mock(side_effect=responses, spec=[])
@@ -215,27 +220,27 @@ class Test_wait_and_retry(object):
         )
 
         assert ret_val == responses[-1]
-
-        assert func.call_count == 3
-        assert func.mock_calls == [mock.call()] * 3
-
-        assert randint_mock.call_count == 2
-        assert randint_mock.mock_calls == [mock.call(0, 1000)] * 2
-
-        assert sleep_mock.call_count == 2
+        assert func.call_count == 4
+        assert func.mock_calls == [mock.call()] * 4
+        assert randint_mock.call_count == 3
+        assert randint_mock.mock_calls == [mock.call(0, 1000)] * 3
+        assert sleep_mock.call_count == 3
         sleep_mock.assert_any_call(1.125)
         sleep_mock.assert_any_call(2.625)
+        sleep_mock.assert_any_call(4.500)
 
     @mock.patch("time.sleep")
     @mock.patch("random.randint")
-    def test_success_with_retry_client_timeout(self, randint_mock, sleep_mock):
-        randint_mock.side_effect = [125, 625, 375]
+    def test_retry_success_urllib3_connection_errors(self, randint_mock, sleep_mock):
+        randint_mock.side_effect = [125, 625, 500, 875, 375]
 
         status_code = int(http.client.OK)
         response = _make_response(status_code)
         responses = [
-            requests.exceptions.Timeout,
-            requests.exceptions.Timeout,
+            urllib3.exceptions.PoolError(None, ""),
+            urllib3.exceptions.ProtocolError,
+            urllib3.exceptions.SSLError,
+            urllib3.exceptions.TimeoutError,
             response,
         ]
         func = mock.Mock(side_effect=responses, spec=[])
@@ -246,16 +251,15 @@ class Test_wait_and_retry(object):
         )
 
         assert ret_val == responses[-1]
-
-        assert func.call_count == 3
-        assert func.mock_calls == [mock.call()] * 3
-
-        assert randint_mock.call_count == 2
-        assert randint_mock.mock_calls == [mock.call(0, 1000)] * 2
-
-        assert sleep_mock.call_count == 2
+        assert func.call_count == 5
+        assert func.mock_calls == [mock.call()] * 5
+        assert randint_mock.call_count == 4
+        assert randint_mock.mock_calls == [mock.call(0, 1000)] * 4
+        assert sleep_mock.call_count == 4
         sleep_mock.assert_any_call(1.125)
         sleep_mock.assert_any_call(2.625)
+        sleep_mock.assert_any_call(4.500)
+        sleep_mock.assert_any_call(8.875)
 
     @mock.patch("time.sleep")
     @mock.patch("random.randint")