Skip to content

Commit a2bd2a5

Browse files
authored
fix: Include ConnectionError and urllib3 exception as retriable (#282)
* fix: Include ConnectionError and urllib3 exception as retriable * fix: include Py3 ConnectionError class as retriable, and refactor
1 parent 494aa06 commit a2bd2a5

6 files changed

Lines changed: 398 additions & 424 deletions

File tree

packages/google-resumable-media/google/resumable_media/_helpers.py

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import hashlib
2121
import logging
2222
import random
23-
import time
2423
import warnings
2524

2625
from google.resumable_media import common
@@ -135,78 +134,6 @@ def calculate_retry_wait(base_wait, max_sleep, multiplier=2.0):
135134
return new_base_wait, new_base_wait + 0.001 * jitter_ms
136135

137136

138-
def wait_and_retry(func, get_status_code, retry_strategy):
139-
"""Attempts to retry a call to ``func`` until success.
140-
141-
Expects ``func`` to return an HTTP response and uses ``get_status_code``
142-
to check if the response is retry-able.
143-
144-
``func`` is expected to raise a failure status code as a
145-
common.InvalidResponse, at which point this method will check the code
146-
against the common.RETRIABLE list of retriable status codes.
147-
148-
Will retry until :meth:`~.RetryStrategy.retry_allowed` (on the current
149-
``retry_strategy``) returns :data:`False`. Uses
150-
:func:`calculate_retry_wait` to double the wait time (with jitter) after
151-
each attempt.
152-
153-
Args:
154-
func (Callable): A callable that takes no arguments and produces
155-
an HTTP response which will be checked as retry-able.
156-
get_status_code (Callable[Any, int]): Helper to get a status code
157-
from a response.
158-
retry_strategy (~google.resumable_media.common.RetryStrategy): The
159-
strategy to use if the request fails and must be retried.
160-
161-
Returns:
162-
object: The return value of ``func``.
163-
"""
164-
total_sleep = 0.0
165-
num_retries = 0
166-
# base_wait will be multiplied by the multiplier on the first retry.
167-
base_wait = float(retry_strategy.initial_delay) / retry_strategy.multiplier
168-
169-
# Set the retriable_exception_type if possible. We expect requests to be
170-
# present here and the transport to be using requests.exceptions errors,
171-
# but due to loose coupling with the transport layer we can't guarantee it.
172-
try:
173-
connection_error_exceptions = _get_connection_error_classes()
174-
except ImportError:
175-
# We don't know the correct classes to use to catch connection errors,
176-
# so an empty tuple here communicates "catch no exceptions".
177-
connection_error_exceptions = ()
178-
179-
while True: # return on success or when retries exhausted.
180-
error = None
181-
try:
182-
response = func()
183-
except connection_error_exceptions as e:
184-
error = e # Fall through to retry, if there are retries left.
185-
except common.InvalidResponse as e:
186-
# An InvalidResponse is only retriable if its status code matches.
187-
# The `process_response()` method on a Download or Upload method
188-
# will convert the status code into an exception.
189-
if get_status_code(e.response) in common.RETRYABLE:
190-
error = e # Fall through to retry, if there are retries left.
191-
else:
192-
raise # If the status code is not retriable, raise w/o retry.
193-
else:
194-
return response
195-
196-
base_wait, wait_time = calculate_retry_wait(
197-
base_wait, retry_strategy.max_sleep, retry_strategy.multiplier
198-
)
199-
num_retries += 1
200-
total_sleep += wait_time
201-
202-
# Check if (another) retry is allowed. If retries are exhausted and
203-
# no acceptable response was received, raise the retriable error.
204-
if not retry_strategy.retry_allowed(total_sleep, num_retries):
205-
raise error
206-
207-
time.sleep(wait_time)
208-
209-
210137
def _get_crc32c_object():
211138
"""Get crc32c object
212139
Attempt to use the Google-CRC32c package. If it isn't available, try
@@ -375,22 +302,6 @@ def _get_checksum_object(checksum_type):
375302
raise ValueError("checksum must be ``'md5'``, ``'crc32c'`` or ``None``")
376303

377304

378-
def _get_connection_error_classes():
379-
"""Get the exception error classes.
380-
381-
Requests is a soft dependency here so that multiple transport layers can be
382-
added in the future. This code is in a separate function here so that the
383-
test framework can override its behavior to simulate requests being
384-
unavailable."""
385-
386-
import requests.exceptions
387-
388-
return (
389-
requests.exceptions.ConnectionError,
390-
requests.exceptions.ChunkedEncodingError,
391-
)
392-
393-
394305
class _DoNothingHash(object):
395306
"""Do-nothing hash object.
396307

packages/google-resumable-media/google/resumable_media/requests/_request_helpers.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
This utilities are explicitly catered to ``requests``-like transports.
1818
"""
1919

20+
import requests.exceptions
21+
import urllib3.exceptions
2022

21-
from google.resumable_media import common
23+
import time
2224

25+
from google.resumable_media import common
26+
from google.resumable_media import _helpers
2327

2428
_DEFAULT_RETRY_STRATEGY = common.RetryStrategy()
2529
_SINGLE_GET_CHUNK_SIZE = 8192
@@ -30,6 +34,13 @@
3034
# The number of seconds to wait between bytes sent from the server.
3135
_DEFAULT_READ_TIMEOUT = 60
3236

37+
_CONNECTION_ERROR_CLASSES = (
38+
requests.exceptions.ConnectionError,
39+
requests.exceptions.ChunkedEncodingError,
40+
urllib3.exceptions.ProtocolError,
41+
ConnectionError, # Python 3.x only, superclass of ConnectionResetError.
42+
)
43+
3344

3445
class RequestsMixin(object):
3546
"""Mix-in class implementing ``requests``-specific behavior.
@@ -93,3 +104,69 @@ def _get_body(response):
93104
)
94105
response._content_consumed = True
95106
return response._content
107+
108+
109+
def wait_and_retry(func, get_status_code, retry_strategy):
110+
"""Attempts to retry a call to ``func`` until success.
111+
112+
Expects ``func`` to return an HTTP response and uses ``get_status_code``
113+
to check if the response is retry-able.
114+
115+
``func`` is expected to raise a failure status code as a
116+
common.InvalidResponse, at which point this method will check the code
117+
against the common.RETRIABLE list of retriable status codes.
118+
119+
Will retry until :meth:`~.RetryStrategy.retry_allowed` (on the current
120+
``retry_strategy``) returns :data:`False`. Uses
121+
:func:`_helpers.calculate_retry_wait` to double the wait time (with jitter)
122+
after each attempt.
123+
124+
Args:
125+
func (Callable): A callable that takes no arguments and produces
126+
an HTTP response which will be checked as retry-able.
127+
get_status_code (Callable[Any, int]): Helper to get a status code
128+
from a response.
129+
retry_strategy (~google.resumable_media.common.RetryStrategy): The
130+
strategy to use if the request fails and must be retried.
131+
132+
Returns:
133+
object: The return value of ``func``.
134+
"""
135+
total_sleep = 0.0
136+
num_retries = 0
137+
# base_wait will be multiplied by the multiplier on the first retry.
138+
base_wait = float(retry_strategy.initial_delay) / retry_strategy.multiplier
139+
140+
# Set the retriable_exception_type if possible. We expect requests to be
141+
# present here and the transport to be using requests.exceptions errors,
142+
# but due to loose coupling with the transport layer we can't guarantee it.
143+
144+
while True: # return on success or when retries exhausted.
145+
error = None
146+
try:
147+
response = func()
148+
except _CONNECTION_ERROR_CLASSES as e:
149+
error = e # Fall through to retry, if there are retries left.
150+
except common.InvalidResponse as e:
151+
# An InvalidResponse is only retriable if its status code matches.
152+
# The `process_response()` method on a Download or Upload method
153+
# will convert the status code into an exception.
154+
if get_status_code(e.response) in common.RETRYABLE:
155+
error = e # Fall through to retry, if there are retries left.
156+
else:
157+
raise # If the status code is not retriable, raise w/o retry.
158+
else:
159+
return response
160+
161+
base_wait, wait_time = _helpers.calculate_retry_wait(
162+
base_wait, retry_strategy.max_sleep, retry_strategy.multiplier
163+
)
164+
num_retries += 1
165+
total_sleep += wait_time
166+
167+
# Check if (another) retry is allowed. If retries are exhausted and
168+
# no acceptable response was received, raise the retriable error.
169+
if not retry_strategy.retry_allowed(total_sleep, num_retries):
170+
raise error
171+
172+
time.sleep(wait_time)

packages/google-resumable-media/google/resumable_media/requests/download.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def retriable_request():
171171

172172
return result
173173

174-
return _helpers.wait_and_retry(
174+
return _request_helpers.wait_and_retry(
175175
retriable_request, self._get_status_code, self._retry_strategy
176176
)
177177

@@ -306,7 +306,7 @@ def retriable_request():
306306

307307
return result
308308

309-
return _helpers.wait_and_retry(
309+
return _request_helpers.wait_and_retry(
310310
retriable_request, self._get_status_code, self._retry_strategy
311311
)
312312

@@ -381,7 +381,7 @@ def retriable_request():
381381
self._process_response(result)
382382
return result
383383

384-
return _helpers.wait_and_retry(
384+
return _request_helpers.wait_and_retry(
385385
retriable_request, self._get_status_code, self._retry_strategy
386386
)
387387

@@ -457,7 +457,7 @@ def retriable_request():
457457
self._process_response(result)
458458
return result
459459

460-
return _helpers.wait_and_retry(
460+
return _request_helpers.wait_and_retry(
461461
retriable_request, self._get_status_code, self._retry_strategy
462462
)
463463

packages/google-resumable-media/google/resumable_media/requests/upload.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121

2222
from google.resumable_media import _upload
23-
from google.resumable_media import _helpers
2423
from google.resumable_media.requests import _request_helpers
2524

2625

@@ -80,7 +79,7 @@ def retriable_request():
8079

8180
return result
8281

83-
return _helpers.wait_and_retry(
82+
return _request_helpers.wait_and_retry(
8483
retriable_request, self._get_status_code, self._retry_strategy
8584
)
8685

@@ -151,7 +150,7 @@ def retriable_request():
151150

152151
return result
153152

154-
return _helpers.wait_and_retry(
153+
return _request_helpers.wait_and_retry(
155154
retriable_request, self._get_status_code, self._retry_strategy
156155
)
157156

@@ -418,7 +417,7 @@ def retriable_request():
418417

419418
return result
420419

421-
return _helpers.wait_and_retry(
420+
return _request_helpers.wait_and_retry(
422421
retriable_request, self._get_status_code, self._retry_strategy
423422
)
424423

@@ -513,7 +512,7 @@ def retriable_request():
513512

514513
return result
515514

516-
return _helpers.wait_and_retry(
515+
return _request_helpers.wait_and_retry(
517516
retriable_request, self._get_status_code, self._retry_strategy
518517
)
519518

@@ -552,6 +551,6 @@ def retriable_request():
552551

553552
return result
554553

555-
return _helpers.wait_and_retry(
554+
return _request_helpers.wait_and_retry(
556555
retriable_request, self._get_status_code, self._retry_strategy
557556
)

0 commit comments

Comments
 (0)