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"""Helpers for retrying functions with exponential back-off. 
    16 
    17The :class:`Retry` decorator can be used to retry functions that raise 
    18exceptions using exponential backoff. Because a exponential sleep algorithm is 
    19used, the retry is limited by a `timeout`. The timeout determines the window 
    20in which retries will be attempted. This is used instead of total number of retries 
    21because it is difficult to ascertain the amount of time a function can block 
    22when using total number of retries and exponential backoff. 
    23 
    24By default, this decorator will retry transient 
    25API 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 
    36You can pass a custom predicate to retry on different exceptions, such as 
    37waiting 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 
    47Some client library methods apply retry automatically. These methods can accept 
    48a ``retry`` parameter that allows you to configure the behavior: 
    49 
    50.. code-block:: python 
    51 
    52    my_retry = retry.Retry(timeout=60) 
    53    result = client.some_method(retry=my_retry) 
    54 
    55""" 
    56 
    57from __future__ import annotations 
    58 
    59import functools 
    60import sys 
    61import time 
    62import inspect 
    63import warnings 
    64from typing import Any, Callable, Iterable, TypeVar, TYPE_CHECKING 
    65 
    66from google.api_core.retry.retry_base import _BaseRetry 
    67from google.api_core.retry.retry_base import _retry_error_helper 
    68from google.api_core.retry.retry_base import exponential_sleep_generator 
    69from google.api_core.retry.retry_base import build_retry_error 
    70from google.api_core.retry.retry_base import RetryFailureReason 
    71 
    72 
    73if TYPE_CHECKING: 
    74    if sys.version_info >= (3, 10): 
    75        from typing import ParamSpec 
    76    else: 
    77        from typing_extensions import ParamSpec 
    78 
    79    _P = ParamSpec("_P")  # target function call parameters 
    80    _R = TypeVar("_R")  # target function returned value 
    81 
    82_ASYNC_RETRY_WARNING = "Using the synchronous google.api_core.retry.Retry with asynchronous calls may lead to unexpected results. Please use google.api_core.retry_async.AsyncRetry instead." 
    83 
    84 
    85def retry_target( 
    86    target: Callable[[], _R], 
    87    predicate: Callable[[Exception], bool], 
    88    sleep_generator: Iterable[float], 
    89    timeout: float | None = None, 
    90    on_error: Callable[[Exception], None] | None = None, 
    91    exception_factory: Callable[ 
    92        [list[Exception], RetryFailureReason, float | None], 
    93        tuple[Exception, Exception | None], 
    94    ] = build_retry_error, 
    95    **kwargs, 
    96): 
    97    """Call a function and retry if it fails. 
    98 
    99    This is the lowest-level retry helper. Generally, you'll use the 
    100    higher-level retry helper :class:`Retry`. 
    101 
    102    Args: 
    103        target(Callable): The function to call and retry. This must be a 
    104            nullary function - apply arguments with `functools.partial`. 
    105        predicate (Callable[Exception]): A callable used to determine if an 
    106            exception raised by the target should be considered retryable. 
    107            It should return True to retry or False otherwise. 
    108        sleep_generator (Iterable[float]): An infinite iterator that determines 
    109            how long to sleep between retries. 
    110        timeout (Optional[float]): How long to keep retrying the target. 
    111            Note: timeout is only checked before initiating a retry, so the target may 
    112            run past the timeout value as long as it is healthy. 
    113        on_error (Optional[Callable[Exception]]): If given, the on_error 
    114            callback will be called with each retryable exception raised by the 
    115            target. Any error raised by this function will *not* be caught. 
    116        exception_factory: A function that is called when the retryable reaches 
    117            a terminal failure state, used to construct an exception to be raised. 
    118            It takes a list of all exceptions encountered, a retry.RetryFailureReason 
    119            enum indicating the failure cause, and the original timeout value 
    120            as arguments. It should return a tuple of the exception to be raised, 
    121            along with the cause exception if any. The default implementation will raise 
    122            a RetryError on timeout, or the last exception encountered otherwise. 
    123        deadline (float): DEPRECATED: use ``timeout`` instead. For backward 
    124            compatibility, if specified it will override ``timeout`` parameter. 
    125 
    126    Returns: 
    127        Any: the return value of the target function. 
    128 
    129    Raises: 
    130        ValueError: If the sleep generator stops yielding values. 
    131        Exception: a custom exception specified by the exception_factory if provided. 
    132            If no exception_factory is provided: 
    133                google.api_core.RetryError: If the timeout is exceeded while retrying. 
    134                Exception: If the target raises an error that isn't retryable. 
    135    """ 
    136 
    137    timeout = kwargs.get("deadline", timeout) 
    138 
    139    deadline = time.monotonic() + timeout if timeout is not None else None 
    140    error_list: list[Exception] = [] 
    141    sleep_iter = iter(sleep_generator) 
    142 
    143    # continue trying until an attempt completes, or a terminal exception is raised in _retry_error_helper 
    144    # TODO: support max_attempts argument: https://github.com/googleapis/python-api-core/issues/535 
    145    while True: 
    146        try: 
    147            result = target() 
    148            if inspect.isawaitable(result): 
    149                warnings.warn(_ASYNC_RETRY_WARNING) 
    150            return result 
    151 
    152        # pylint: disable=broad-except 
    153        # This function explicitly must deal with broad exceptions. 
    154        except Exception as exc: 
    155            # defer to shared logic for handling errors 
    156            next_sleep = _retry_error_helper( 
    157                exc, 
    158                deadline, 
    159                sleep_iter, 
    160                error_list, 
    161                predicate, 
    162                on_error, 
    163                exception_factory, 
    164                timeout, 
    165            ) 
    166            # if exception not raised, sleep before next attempt 
    167            time.sleep(next_sleep) 
    168 
    169 
    170class Retry(_BaseRetry): 
    171    """Exponential retry decorator for unary synchronous RPCs. 
    172 
    173    This class is a decorator used to add retry or polling behavior to an RPC 
    174    call. 
    175 
    176    Although the default behavior is to retry transient API errors, a 
    177    different predicate can be provided to retry other exceptions. 
    178 
    179    There are two important concepts that retry/polling behavior may operate on, 
    180    Deadline and Timeout, which need to be properly defined for the correct 
    181    usage of this class and the rest of the library. 
    182 
    183    Deadline: a fixed point in time by which a certain operation must 
    184    terminate. For example, if a certain operation has a deadline 
    185    "2022-10-18T23:30:52.123Z" it must terminate (successfully or with an 
    186    error) by that time, regardless of when it was started or whether it 
    187    was started at all. 
    188 
    189    Timeout: the maximum duration of time after which a certain operation 
    190    must terminate (successfully or with an error). The countdown begins right 
    191    after an operation was started. For example, if an operation was started at 
    192    09:24:00 with timeout of 75 seconds, it must terminate no later than 
    193    09:25:15. 
    194 
    195    Unfortunately, in the past this class (and the api-core library as a whole) has not 
    196    been properly distinguishing the concepts of "timeout" and "deadline", and the 
    197    ``deadline`` parameter has meant ``timeout``. That is why 
    198    ``deadline`` has been deprecated and ``timeout`` should be used instead. If the 
    199    ``deadline`` parameter is set, it will override the ``timeout`` parameter. 
    200    In other words, ``retry.deadline`` should be treated as just a deprecated alias for 
    201    ``retry.timeout``. 
    202 
    203    Said another way, it is safe to assume that this class and the rest of this 
    204    library operate in terms of timeouts (not deadlines) unless explicitly 
    205    noted the usage of deadline semantics. 
    206 
    207    It is also important to 
    208    understand the three most common applications of the Timeout concept in the 
    209    context of this library. 
    210 
    211    Usually the generic Timeout term may stand for one of the following actual 
    212    timeouts: RPC Timeout, Retry Timeout, or Polling Timeout. 
    213 
    214    RPC Timeout: a value supplied by the client to the server so 
    215    that the server side knows the maximum amount of time it is expected to 
    216    spend handling that specific RPC. For example, in the case of gRPC transport, 
    217    RPC Timeout is represented by setting "grpc-timeout" header in the HTTP2 
    218    request. The `timeout` property of this class normally never represents the 
    219    RPC Timeout as it is handled separately by the ``google.api_core.timeout`` 
    220    module of this library. 
    221 
    222    Retry Timeout: this is the most common meaning of the ``timeout`` property 
    223    of this class, and defines how long a certain RPC may be retried in case 
    224    the server returns an error. 
    225 
    226    Polling Timeout: defines how long the 
    227    client side is allowed to call the polling RPC repeatedly to check a status of a 
    228    long-running operation. Each polling RPC is 
    229    expected to succeed (its errors are supposed to be handled by the retry 
    230    logic). The decision as to whether a new polling attempt needs to be made is based 
    231    not on the RPC status code but  on the status of the returned 
    232    status of an operation. In other words: we will poll a long-running operation until 
    233    the operation is done or the polling timeout expires. Each poll will inform us of 
    234    the status of the operation. The poll consists of an RPC to the server that may 
    235    itself be retried as per the poll-specific retry settings in case of errors. The 
    236    operation-level retry settings do NOT apply to polling-RPC retries. 
    237 
    238    With the actual timeout types being defined above, the client libraries 
    239    often refer to just Timeout without clarifying which type specifically 
    240    that is. In that case the actual timeout type (sometimes also referred to as 
    241    Logical Timeout) can be determined from the context. If it is a unary rpc 
    242    call (i.e. a regular one) Timeout usually stands for the RPC Timeout (if 
    243    provided directly as a standalone value) or Retry Timeout (if provided as 
    244    ``retry.timeout`` property of the unary RPC's retry config). For 
    245    ``Operation`` or ``PollingFuture`` in general Timeout stands for 
    246    Polling Timeout. 
    247 
    248    Args: 
    249        predicate (Callable[Exception]): A callable that should return ``True`` 
    250            if the given exception is retryable. 
    251        initial (float): The minimum amount of time to delay in seconds. This 
    252            must be greater than 0. 
    253        maximum (float): The maximum amount of time to delay in seconds. 
    254        multiplier (float): The multiplier applied to the delay. 
    255        timeout (Optional[float]): How long to keep retrying, in seconds. 
    256            Note: timeout is only checked before initiating a retry, so the target may 
    257            run past the timeout value as long as it is healthy. 
    258        on_error (Callable[Exception]): A function to call while processing 
    259            a retryable exception. Any error raised by this function will 
    260            *not* be caught. 
    261        deadline (float): DEPRECATED: use `timeout` instead. For backward 
    262            compatibility, if specified it will override the ``timeout`` parameter. 
    263    """ 
    264 
    265    def __call__( 
    266        self, 
    267        func: Callable[_P, _R], 
    268        on_error: Callable[[Exception], Any] | None = None, 
    269    ) -> Callable[_P, _R]: 
    270        """Wrap a callable with retry behavior. 
    271 
    272        Args: 
    273            func (Callable): The callable to add retry behavior to. 
    274            on_error (Optional[Callable[Exception]]): If given, the 
    275                on_error callback will be called with each retryable exception 
    276                raised by the wrapped function. Any error raised by this 
    277                function will *not* be caught. If on_error was specified in the 
    278                constructor, this value will be ignored. 
    279 
    280        Returns: 
    281            Callable: A callable that will invoke ``func`` with retry 
    282                behavior. 
    283        """ 
    284        if self._on_error is not None: 
    285            on_error = self._on_error 
    286 
    287        @functools.wraps(func) 
    288        def retry_wrapped_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: 
    289            """A wrapper that calls target function with retry.""" 
    290            target = functools.partial(func, *args, **kwargs) 
    291            sleep_generator = exponential_sleep_generator( 
    292                self._initial, self._maximum, multiplier=self._multiplier 
    293            ) 
    294            return retry_target( 
    295                target, 
    296                self._predicate, 
    297                sleep_generator, 
    298                timeout=self._timeout, 
    299                on_error=on_error, 
    300            ) 
    301 
    302        return retry_wrapped_func