1# Copyright 2017 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Shared classes and functions for retrying requests.
16
17:class:`_BaseRetry` is the base class for :class:`Retry`,
18:class:`AsyncRetry`, :class:`StreamingRetry`, and :class:`AsyncStreamingRetry`.
19"""
20
21from __future__ import annotations
22
23import logging
24import random
25import time
26
27from enum import Enum
28from typing import Any, Callable, Optional, Iterator, TYPE_CHECKING
29
30import requests.exceptions
31
32from google.api_core import exceptions
33from google.auth import exceptions as auth_exceptions
34
35if TYPE_CHECKING:
36 import sys
37
38 if sys.version_info >= (3, 11):
39 from typing import Self
40 else:
41 from typing_extensions import Self
42
43_DEFAULT_INITIAL_DELAY = 1.0 # seconds
44_DEFAULT_MAXIMUM_DELAY = 60.0 # seconds
45_DEFAULT_DELAY_MULTIPLIER = 2.0
46_DEFAULT_DEADLINE = 60.0 * 2.0 # seconds
47
48_LOGGER = logging.getLogger("google.api_core.retry")
49
50
51def if_exception_type(
52 *exception_types: type[Exception],
53) -> Callable[[Exception], bool]:
54 """Creates a predicate to check if the exception is of a given type.
55
56 Args:
57 exception_types (Sequence[:func:`type`]): The exception types to check
58 for.
59
60 Returns:
61 Callable[Exception]: A predicate that returns True if the provided
62 exception is of the given type(s).
63 """
64
65 def if_exception_type_predicate(exception: Exception) -> bool:
66 """Bound predicate for checking an exception type."""
67 return isinstance(exception, exception_types)
68
69 return if_exception_type_predicate
70
71
72# pylint: disable=invalid-name
73# Pylint sees this as a constant, but it is also an alias that should be
74# considered a function.
75if_transient_error = if_exception_type(
76 exceptions.InternalServerError,
77 exceptions.TooManyRequests,
78 exceptions.ServiceUnavailable,
79 requests.exceptions.ConnectionError,
80 requests.exceptions.ChunkedEncodingError,
81 auth_exceptions.TransportError,
82)
83"""A predicate that checks if an exception is a transient API error.
84
85The following server errors are considered transient:
86
87- :class:`google.api_core.exceptions.InternalServerError` - HTTP 500, gRPC
88 ``INTERNAL(13)`` and its subclasses.
89- :class:`google.api_core.exceptions.TooManyRequests` - HTTP 429
90- :class:`google.api_core.exceptions.ServiceUnavailable` - HTTP 503
91- :class:`requests.exceptions.ConnectionError`
92- :class:`requests.exceptions.ChunkedEncodingError` - The server declared
93 chunked encoding but sent an invalid chunk.
94- :class:`google.auth.exceptions.TransportError` - Used to indicate an
95 error occurred during an HTTP request.
96"""
97# pylint: enable=invalid-name
98
99
100def exponential_sleep_generator(
101 initial: float, maximum: float, multiplier: float = _DEFAULT_DELAY_MULTIPLIER
102):
103 """Generates sleep intervals based on the exponential back-off algorithm.
104
105 This implements the `Truncated Exponential Back-off`_ algorithm.
106
107 .. _Truncated Exponential Back-off:
108 https://cloud.google.com/storage/docs/exponential-backoff
109
110 Args:
111 initial (float): The minimum amount of time to delay. This must
112 be greater than 0.
113 maximum (float): The maximum amount of time to delay.
114 multiplier (float): The multiplier applied to the delay.
115
116 Yields:
117 float: successive sleep intervals.
118 """
119 max_delay = min(initial, maximum)
120 while True:
121 yield random.uniform(0.0, max_delay)
122 max_delay = min(max_delay * multiplier, maximum)
123
124
125class RetryFailureReason(Enum):
126 """
127 The cause of a failed retry, used when building exceptions
128 """
129
130 TIMEOUT = 0
131 NON_RETRYABLE_ERROR = 1
132
133
134def build_retry_error(
135 exc_list: list[Exception],
136 reason: RetryFailureReason,
137 timeout_val: float | None,
138 **kwargs: Any,
139) -> tuple[Exception, Exception | None]:
140 """
141 Default exception_factory implementation.
142
143 Returns a RetryError if the failure is due to a timeout, otherwise
144 returns the last exception encountered.
145
146 Args:
147 - exc_list: list of exceptions that occurred during the retry
148 - reason: reason for the retry failure.
149 Can be TIMEOUT or NON_RETRYABLE_ERROR
150 - timeout_val: the original timeout value for the retry (in seconds), for use in the exception message
151
152 Returns:
153 - tuple: a tuple of the exception to be raised, and the cause exception if any
154 """
155 if reason == RetryFailureReason.TIMEOUT:
156 # return RetryError with the most recent exception as the cause
157 src_exc = exc_list[-1] if exc_list else None
158 timeout_val_str = f"of {timeout_val:0.1f}s " if timeout_val is not None else ""
159 return (
160 exceptions.RetryError(
161 f"Timeout {timeout_val_str}exceeded",
162 src_exc,
163 ),
164 src_exc,
165 )
166 elif exc_list:
167 # return most recent exception encountered and its cause
168 final_exc = exc_list[-1]
169 cause = getattr(final_exc, "__cause__", None)
170 return final_exc, cause
171 else:
172 # no exceptions were given in exc_list. Raise generic RetryError
173 return exceptions.RetryError("Unknown error", None), None
174
175
176def _retry_error_helper(
177 exc: Exception,
178 deadline: float | None,
179 sleep_iterator: Iterator[float],
180 error_list: list[Exception],
181 predicate_fn: Callable[[Exception], bool],
182 on_error_fn: Callable[[Exception], None] | None,
183 exc_factory_fn: Callable[
184 [list[Exception], RetryFailureReason, float | None],
185 tuple[Exception, Exception | None],
186 ],
187 original_timeout: float | None,
188) -> float:
189 """
190 Shared logic for handling an error for all retry implementations
191
192 - Raises an error on timeout or non-retryable error
193 - Calls on_error_fn if provided
194 - Logs the error
195
196 Args:
197 - exc: the exception that was raised
198 - deadline: the deadline for the retry, calculated as a diff from time.monotonic()
199 - sleep_iterator: iterator to draw the next backoff value from
200 - error_list: the list of exceptions that have been raised so far
201 - predicate_fn: takes `exc` and returns true if the operation should be retried
202 - on_error_fn: callback to execute when a retryable error occurs
203 - exc_factory_fn: callback used to build the exception to be raised on terminal failure
204 - original_timeout_val: the original timeout value for the retry (in seconds),
205 to be passed to the exception factory for building an error message
206 Returns:
207 - the sleep value chosen before the next attempt
208 """
209 error_list.append(exc)
210 if not predicate_fn(exc):
211 final_exc, source_exc = exc_factory_fn(
212 error_list,
213 RetryFailureReason.NON_RETRYABLE_ERROR,
214 original_timeout,
215 )
216 raise final_exc from source_exc
217 if on_error_fn is not None:
218 on_error_fn(exc)
219 # next_sleep is fetched after the on_error callback, to allow clients
220 # to update sleep_iterator values dynamically in response to errors
221 try:
222 next_sleep = next(sleep_iterator)
223 except StopIteration:
224 raise ValueError("Sleep generator stopped yielding sleep values.") from exc
225 if deadline is not None and time.monotonic() + next_sleep > deadline:
226 final_exc, source_exc = exc_factory_fn(
227 error_list,
228 RetryFailureReason.TIMEOUT,
229 original_timeout,
230 )
231 raise final_exc from source_exc
232 _LOGGER.debug(
233 "Retrying due to {}, sleeping {:.1f}s ...".format(error_list[-1], next_sleep)
234 )
235 return next_sleep
236
237
238class _BaseRetry(object):
239 """
240 Base class for retry configuration objects. This class is intended to capture retry
241 and backoff configuration that is common to both synchronous and asynchronous retries,
242 for both unary and streaming RPCs. It is not intended to be instantiated directly,
243 but rather to be subclassed by the various retry configuration classes.
244 """
245
246 def __init__(
247 self,
248 predicate: Callable[[Exception], bool] = if_transient_error,
249 initial: float = _DEFAULT_INITIAL_DELAY,
250 maximum: float = _DEFAULT_MAXIMUM_DELAY,
251 multiplier: float = _DEFAULT_DELAY_MULTIPLIER,
252 timeout: Optional[float] = _DEFAULT_DEADLINE,
253 on_error: Optional[Callable[[Exception], Any]] = None,
254 **kwargs: Any,
255 ) -> None:
256 self._predicate = predicate
257 self._initial = initial
258 self._multiplier = multiplier
259 self._maximum = maximum
260 self._timeout = kwargs.get("deadline", timeout)
261 self._deadline = self._timeout
262 self._on_error = on_error
263
264 def __call__(self, *args, **kwargs) -> Any:
265 raise NotImplementedError("Not implemented in base class")
266
267 @property
268 def deadline(self) -> float | None:
269 """
270 DEPRECATED: use ``timeout`` instead. Refer to the ``Retry`` class
271 documentation for details.
272 """
273 return self._timeout
274
275 @property
276 def timeout(self) -> float | None:
277 return self._timeout
278
279 def with_deadline(self, deadline: float | None) -> Self:
280 """Return a copy of this retry with the given timeout.
281
282 DEPRECATED: use :meth:`with_timeout` instead. Refer to the ``Retry`` class
283 documentation for details.
284
285 Args:
286 deadline (float|None): How long to keep retrying, in seconds. If None,
287 no timeout is enforced.
288
289 Returns:
290 Retry: A new retry instance with the given timeout.
291 """
292 return self.with_timeout(deadline)
293
294 def with_timeout(self, timeout: float | None) -> Self:
295 """Return a copy of this retry with the given timeout.
296
297 Args:
298 timeout (float): How long to keep retrying, in seconds. If None,
299 no timeout will be enforced.
300
301 Returns:
302 Retry: A new retry instance with the given timeout.
303 """
304 return type(self)(
305 predicate=self._predicate,
306 initial=self._initial,
307 maximum=self._maximum,
308 multiplier=self._multiplier,
309 timeout=timeout,
310 on_error=self._on_error,
311 )
312
313 def with_predicate(self, predicate: Callable[[Exception], bool]) -> Self:
314 """Return a copy of this retry with the given predicate.
315
316 Args:
317 predicate (Callable[Exception]): A callable that should return
318 ``True`` if the given exception is retryable.
319
320 Returns:
321 Retry: A new retry instance with the given predicate.
322 """
323 return type(self)(
324 predicate=predicate,
325 initial=self._initial,
326 maximum=self._maximum,
327 multiplier=self._multiplier,
328 timeout=self._timeout,
329 on_error=self._on_error,
330 )
331
332 def with_delay(
333 self,
334 initial: Optional[float] = None,
335 maximum: Optional[float] = None,
336 multiplier: Optional[float] = None,
337 ) -> Self:
338 """Return a copy of this retry with the given delay options.
339
340 Args:
341 initial (float): The minimum amount of time to delay (in seconds). This must
342 be greater than 0. If None, the current value is used.
343 maximum (float): The maximum amount of time to delay (in seconds). If None, the
344 current value is used.
345 multiplier (float): The multiplier applied to the delay. If None, the current
346 value is used.
347
348 Returns:
349 Retry: A new retry instance with the given delay options.
350 """
351 return type(self)(
352 predicate=self._predicate,
353 initial=initial if initial is not None else self._initial,
354 maximum=maximum if maximum is not None else self._maximum,
355 multiplier=multiplier if multiplier is not None else self._multiplier,
356 timeout=self._timeout,
357 on_error=self._on_error,
358 )
359
360 def __str__(self) -> str:
361 return (
362 "<{} predicate={}, initial={:.1f}, maximum={:.1f}, "
363 "multiplier={:.1f}, timeout={}, on_error={}>".format(
364 type(self).__name__,
365 self._predicate,
366 self._initial,
367 self._maximum,
368 self._multiplier,
369 self._timeout, # timeout can be None, thus no {:.1f}
370 self._on_error,
371 )
372 )