Skip to content

Commit 71466f8

Browse files
author
Jon Wayne Parrott
authored
Add google.api.core.retry.Retry decorator (#3835)
* Add google.api.core.retry.Retry decorator * Add futures dependency * Change jitter algorithm
1 parent bf66cb6 commit 71466f8

File tree

3 files changed

+287
-28
lines changed

3 files changed

+287
-28
lines changed

packages/google-cloud-core/google/api/core/retry.py

Lines changed: 172 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,52 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""Helpers for retrying functions with exponential back-off."""
15+
"""Helpers for retrying functions with exponential back-off.
16+
17+
The :cls:`Retry` decorator can be used to retry functions that raise exceptions
18+
using exponential backoff. Because a exponential sleep algorithm is used,
19+
the retry is limited by a `deadline`. The deadline is the maxmimum amount of
20+
time a method can block. This is used instead of total number of retries
21+
because it is difficult to ascertain the amount of time a function can block
22+
when using total number of retries and exponential backoff.
23+
24+
By default, this decorator will retry transient
25+
API errors (see :func:`if_transient_error`). For example:
26+
27+
.. code-block:: python
28+
29+
@retry.Retry()
30+
def call_flaky_rpc():
31+
return client.flaky_rpc()
32+
33+
# Will retry flaky_rpc() if it raises transient API errors.
34+
result = call_flaky_rpc()
35+
36+
You can pass a custom predicate to retry on different exceptions, such as
37+
waiting for an eventually consistent item to be available:
38+
39+
.. code-block:: python
40+
41+
@retry.Retry(predicate=if_exception_type(exceptions.NotFound))
42+
def check_if_exists():
43+
return client.does_thing_exist()
44+
45+
is_available = check_if_exists()
46+
47+
Some client library methods apply retry automatically. These methods can accept
48+
a ``retry`` parameter that allows you to configure the behavior:
49+
50+
.. code-block:: python
51+
52+
my_retry = retry.Retry(deadline=60)
53+
result = client.some_method(retry=my_retry)
54+
55+
"""
56+
57+
from __future__ import unicode_literals
1658

1759
import datetime
60+
import functools
1861
import logging
1962
import random
2063
import time
@@ -25,7 +68,10 @@
2568
from google.api.core.helpers import datetime_helpers
2669

2770
_LOGGER = logging.getLogger(__name__)
28-
_DEFAULT_MAX_JITTER = 0.2
71+
_DEFAULT_INITIAL_DELAY = 1.0
72+
_DEFAULT_MAXIMUM_DELAY = 60.0
73+
_DEFAULT_DELAY_MULTIPLIER = 2.0
74+
_DEFAULT_DEADLINE = 60.0 * 2.0
2975

3076

3177
def if_exception_type(*exception_types):
@@ -38,10 +84,10 @@ def if_exception_type(*exception_types):
3884
Callable[Exception]: A predicate that returns True if the provided
3985
exception is of the given type(s).
4086
"""
41-
def inner(exception):
87+
def if_exception_type_predicate(exception):
4288
"""Bound predicate for checking an exception type."""
4389
return isinstance(exception, exception_types)
44-
return inner
90+
return if_exception_type_predicate
4591

4692

4793
# pylint: disable=invalid-name
@@ -64,7 +110,7 @@ def inner(exception):
64110

65111

66112
def exponential_sleep_generator(
67-
initial, maximum, multiplier=2, jitter=_DEFAULT_MAX_JITTER):
113+
initial, maximum, multiplier=_DEFAULT_DELAY_MULTIPLIER):
68114
"""Generates sleep intervals based on the exponential back-off algorithm.
69115
70116
This implements the `Truncated Exponential Back-off`_ algorithm.
@@ -77,16 +123,16 @@ def exponential_sleep_generator(
77123
be greater than 0.
78124
maximum (float): The maximum about of time to delay.
79125
multiplier (float): The multiplier applied to the delay.
80-
jitter (float): The maximum about of randomness to apply to the delay.
81126
82127
Yields:
83128
float: successive sleep intervals.
84129
"""
85130
delay = initial
86131
while True:
87-
yield delay
88-
delay = min(
89-
delay * multiplier + random.uniform(0, jitter), maximum)
132+
# Introduce jitter by yielding a delay that is uniformly distributed
133+
# to average out to the delay time.
134+
yield min(random.uniform(0.0, delay * 2.0), maximum)
135+
delay = delay * multiplier
90136

91137

92138
def retry_target(target, predicate, sleep_generator, deadline):
@@ -146,3 +192,120 @@ def retry_target(target, predicate, sleep_generator, deadline):
146192
time.sleep(sleep)
147193

148194
raise ValueError('Sleep generator stopped yielding sleep values.')
195+
196+
197+
@six.python_2_unicode_compatible
198+
class Retry(object):
199+
"""Exponential retry decorator.
200+
201+
This class is a decorator used to add exponential back-off retry behavior
202+
to an RPC call.
203+
204+
Although the default behavior is to retry transient API errors, a
205+
different predicate can be provided to retry other exceptions.
206+
207+
Args:
208+
predicate (Callable[Exception]): A callable that should return ``True``
209+
if the given exception is retryable.
210+
initial (float): The minimum about of time to delay in seconds. This
211+
must be greater than 0.
212+
maximum (float): The maximum about of time to delay in seconds.
213+
multiplier (float): The multiplier applied to the delay.
214+
deadline (float): How long to keep retrying in seconds.
215+
"""
216+
def __init__(
217+
self,
218+
predicate=if_transient_error,
219+
initial=_DEFAULT_INITIAL_DELAY,
220+
maximum=_DEFAULT_MAXIMUM_DELAY,
221+
multiplier=_DEFAULT_DELAY_MULTIPLIER,
222+
deadline=_DEFAULT_DEADLINE):
223+
self._predicate = predicate
224+
self._initial = initial
225+
self._multiplier = multiplier
226+
self._maximum = maximum
227+
self._deadline = deadline
228+
229+
def __call__(self, func):
230+
"""Wrap a callable with retry behavior.
231+
232+
Args:
233+
func (Callable): The callable to add retry behavior to.
234+
235+
Returns:
236+
Callable: A callable that will invoke ``func`` with retry
237+
behavior.
238+
"""
239+
@six.wraps(func)
240+
def retry_wrapped_func(*args, **kwargs):
241+
"""A wrapper that calls target function with retry."""
242+
target = functools.partial(func, *args, **kwargs)
243+
sleep_generator = exponential_sleep_generator(
244+
self._initial, self._maximum, multiplier=self._multiplier)
245+
return retry_target(
246+
target,
247+
self._predicate,
248+
sleep_generator,
249+
self._deadline)
250+
251+
return retry_wrapped_func
252+
253+
def with_deadline(self, deadline):
254+
"""Return a copy of this retry with the given deadline.
255+
256+
Args:
257+
deadline (float): How long to keep retrying.
258+
259+
Returns:
260+
Retry: A new retry instance with the given deadline.
261+
"""
262+
return Retry(
263+
predicate=self._predicate,
264+
initial=self._initial,
265+
maximum=self._maximum,
266+
multiplier=self._multiplier,
267+
deadline=deadline)
268+
269+
def with_predicate(self, predicate):
270+
"""Return a copy of this retry with the given predicate.
271+
272+
Args:
273+
predicate (Callable[Exception]): A callable that should return
274+
``True`` if the given exception is retryable.
275+
276+
Returns:
277+
Retry: A new retry instance with the given predicate.
278+
"""
279+
return Retry(
280+
predicate=predicate,
281+
initial=self._initial,
282+
maximum=self._maximum,
283+
multiplier=self._multiplier,
284+
deadline=self._deadline)
285+
286+
def with_delay(
287+
self, initial=None, maximum=None, multiplier=None):
288+
"""Return a copy of this retry with the given delay options.
289+
290+
Args:
291+
initial (float): The minimum about of time to delay. This must
292+
be greater than 0.
293+
maximum (float): The maximum about of time to delay.
294+
multiplier (float): The multiplier applied to the delay.
295+
296+
Returns:
297+
Retry: A new retry instance with the given predicate.
298+
"""
299+
return Retry(
300+
predicate=self._predicate,
301+
initial=initial if initial is not None else self._initial,
302+
maximum=maximum if maximum is not None else self._maximum,
303+
multiplier=multiplier if maximum is not None else self._multiplier,
304+
deadline=self._deadline)
305+
306+
def __str__(self):
307+
return (
308+
'<Retry predicate={}, initial={:.1f}, maximum={:.1f}, '
309+
'multiplier={:.1f}, deadline={:.1f}>'.format(
310+
self._predicate, self._initial, self._maximum,
311+
self._multiplier, self._deadline))

packages/google-cloud-core/setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@
6060
'tenacity >= 4.0.0, <5.0.0dev'
6161
]
6262

63+
EXTRAS_REQUIREMENTS = {
64+
':python_version<"3.2"': ['futures >= 3.0.0'],
65+
}
66+
6367
setup(
6468
name='google-cloud-core',
6569
version='0.26.0',
@@ -72,5 +76,6 @@
7276
],
7377
packages=find_packages(exclude=('tests*',)),
7478
install_requires=REQUIREMENTS,
79+
extras_require=EXTRAS_REQUIREMENTS,
7580
**SETUP_BASE
7681
)

0 commit comments

Comments
 (0)