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"""Decorators for applying timeout arguments to functions. 
    16 
    17These decorators are used to wrap API methods to apply either a 
    18Deadline-dependent (recommended), constant (DEPRECATED) or exponential 
    19(DEPRECATED) timeout argument. 
    20 
    21For example, imagine an API method that can take a while to return results, 
    22such as one that might block until a resource is ready: 
    23 
    24.. code-block:: python 
    25 
    26    def is_thing_ready(timeout=None): 
    27        response = requests.get('https://example.com/is_thing_ready') 
    28        response.raise_for_status() 
    29        return response.json() 
    30 
    31This module allows a function like this to be wrapped so that timeouts are 
    32automatically determined, for example: 
    33 
    34.. code-block:: python 
    35 
    36    timeout_ = timeout.ExponentialTimeout() 
    37    is_thing_ready_with_timeout = timeout_(is_thing_ready) 
    38 
    39    for n in range(10): 
    40        try: 
    41            is_thing_ready_with_timeout({'example': 'data'}) 
    42        except: 
    43            pass 
    44 
    45In this example the first call to ``is_thing_ready`` will have a relatively 
    46small timeout (like 1 second). If the resource is available and the request 
    47completes quickly, the loop exits. But, if the resource isn't yet available 
    48and the request times out, it'll be retried - this time with a larger timeout. 
    49 
    50In the broader context these decorators are typically combined with 
    51:mod:`google.api_core.retry` to implement API methods with a signature that 
    52matches ``api_method(request, timeout=None, retry=None)``. 
    53""" 
    54 
    55from __future__ import unicode_literals 
    56 
    57import datetime 
    58import functools 
    59 
    60from google.api_core import datetime_helpers 
    61 
    62_DEFAULT_INITIAL_TIMEOUT = 5.0  # seconds 
    63_DEFAULT_MAXIMUM_TIMEOUT = 30.0  # seconds 
    64_DEFAULT_TIMEOUT_MULTIPLIER = 2.0 
    65# If specified, must be in seconds. If none, deadline is not used in the 
    66# timeout calculation. 
    67_DEFAULT_DEADLINE = None 
    68 
    69 
    70class TimeToDeadlineTimeout(object): 
    71    """A decorator that decreases timeout set for an RPC based on how much time 
    72    has left till its deadline. The deadline is calculated as 
    73    ``now + initial_timeout`` when this decorator is first called for an rpc. 
    74 
    75    In other words this decorator implements deadline semantics in terms of a 
    76    sequence of decreasing timeouts t0 > t1 > t2 ... tn >= 0. 
    77 
    78    Args: 
    79        timeout (Optional[float]): the timeout (in seconds) to applied to the 
    80            wrapped function. If `None`, the target function is expected to 
    81            never timeout. 
    82    """ 
    83 
    84    def __init__(self, timeout=None, clock=datetime_helpers.utcnow): 
    85        self._timeout = timeout 
    86        self._clock = clock 
    87 
    88    def __call__(self, func): 
    89        """Apply the timeout decorator. 
    90 
    91        Args: 
    92            func (Callable): The function to apply the timeout argument to. 
    93                This function must accept a timeout keyword argument. 
    94 
    95        Returns: 
    96            Callable: The wrapped function. 
    97        """ 
    98 
    99        first_attempt_timestamp = self._clock().timestamp() 
    100 
    101        @functools.wraps(func) 
    102        def func_with_timeout(*args, **kwargs): 
    103            """Wrapped function that adds timeout.""" 
    104 
    105            if self._timeout is not None: 
    106                # All calculations are in seconds 
    107                now_timestamp = self._clock().timestamp() 
    108 
    109                # To avoid usage of nonlocal but still have round timeout 
    110                # numbers for first attempt (in most cases the only attempt made 
    111                # for an RPC. 
    112                if now_timestamp - first_attempt_timestamp < 0.001: 
    113                    now_timestamp = first_attempt_timestamp 
    114 
    115                time_since_first_attempt = now_timestamp - first_attempt_timestamp 
    116                remaining_timeout = self._timeout - time_since_first_attempt 
    117 
    118                # Although the `deadline` parameter in `google.api_core.retry.Retry` 
    119                # is deprecated, and should be treated the same as the `timeout`, 
    120                # it is still possible for the `deadline` argument in 
    121                # `google.api_core.retry.Retry` to be larger than the `timeout`. 
    122                # See https://github.com/googleapis/python-api-core/issues/654 
    123                # Only positive non-zero timeouts are supported. 
    124                # Revert back to the initial timeout for negative or 0 timeout values. 
    125                if remaining_timeout < 1: 
    126                    remaining_timeout = self._timeout 
    127 
    128                kwargs["timeout"] = remaining_timeout 
    129 
    130            return func(*args, **kwargs) 
    131 
    132        return func_with_timeout 
    133 
    134    def __str__(self): 
    135        return "<TimeToDeadlineTimeout timeout={:.1f}>".format(self._timeout) 
    136 
    137 
    138class ConstantTimeout(object): 
    139    """A decorator that adds a constant timeout argument. 
    140 
    141    DEPRECATED: use ``TimeToDeadlineTimeout`` instead. 
    142 
    143    This is effectively equivalent to 
    144    ``functools.partial(func, timeout=timeout)``. 
    145 
    146    Args: 
    147        timeout (Optional[float]): the timeout (in seconds) to applied to the 
    148            wrapped function. If `None`, the target function is expected to 
    149            never timeout. 
    150    """ 
    151 
    152    def __init__(self, timeout=None): 
    153        self._timeout = timeout 
    154 
    155    def __call__(self, func): 
    156        """Apply the timeout decorator. 
    157 
    158        Args: 
    159            func (Callable): The function to apply the timeout argument to. 
    160                This function must accept a timeout keyword argument. 
    161 
    162        Returns: 
    163            Callable: The wrapped function. 
    164        """ 
    165 
    166        @functools.wraps(func) 
    167        def func_with_timeout(*args, **kwargs): 
    168            """Wrapped function that adds timeout.""" 
    169            kwargs["timeout"] = self._timeout 
    170            return func(*args, **kwargs) 
    171 
    172        return func_with_timeout 
    173 
    174    def __str__(self): 
    175        return "<ConstantTimeout timeout={:.1f}>".format(self._timeout) 
    176 
    177 
    178def _exponential_timeout_generator(initial, maximum, multiplier, deadline): 
    179    """A generator that yields exponential timeout values. 
    180 
    181    Args: 
    182        initial (float): The initial timeout. 
    183        maximum (float): The maximum timeout. 
    184        multiplier (float): The multiplier applied to the timeout. 
    185        deadline (float): The overall deadline across all invocations. 
    186 
    187    Yields: 
    188        float: A timeout value. 
    189    """ 
    190    if deadline is not None: 
    191        deadline_datetime = datetime_helpers.utcnow() + datetime.timedelta( 
    192            seconds=deadline 
    193        ) 
    194    else: 
    195        deadline_datetime = datetime.datetime.max 
    196 
    197    timeout = initial 
    198    while True: 
    199        now = datetime_helpers.utcnow() 
    200        yield min( 
    201            # The calculated timeout based on invocations. 
    202            timeout, 
    203            # The set maximum timeout. 
    204            maximum, 
    205            # The remaining time before the deadline is reached. 
    206            float((deadline_datetime - now).seconds), 
    207        ) 
    208        timeout = timeout * multiplier 
    209 
    210 
    211class ExponentialTimeout(object): 
    212    """A decorator that adds an exponentially increasing timeout argument. 
    213 
    214    DEPRECATED: the concept of incrementing timeout exponentially has been 
    215    deprecated. Use ``TimeToDeadlineTimeout`` instead. 
    216 
    217    This is useful if a function is called multiple times. Each time the 
    218    function is called this decorator will calculate a new timeout parameter 
    219    based on the the number of times the function has been called. 
    220 
    221    For example 
    222 
    223    .. code-block:: python 
    224 
    225    Args: 
    226        initial (float): The initial timeout to pass. 
    227        maximum (float): The maximum timeout for any one call. 
    228        multiplier (float): The multiplier applied to the timeout for each 
    229            invocation. 
    230        deadline (Optional[float]): The overall deadline across all 
    231            invocations. This is used to prevent a very large calculated 
    232            timeout from pushing the overall execution time over the deadline. 
    233            This is especially useful in conjunction with 
    234            :mod:`google.api_core.retry`. If ``None``, the timeouts will not 
    235            be adjusted to accommodate an overall deadline. 
    236    """ 
    237 
    238    def __init__( 
    239        self, 
    240        initial=_DEFAULT_INITIAL_TIMEOUT, 
    241        maximum=_DEFAULT_MAXIMUM_TIMEOUT, 
    242        multiplier=_DEFAULT_TIMEOUT_MULTIPLIER, 
    243        deadline=_DEFAULT_DEADLINE, 
    244    ): 
    245        self._initial = initial 
    246        self._maximum = maximum 
    247        self._multiplier = multiplier 
    248        self._deadline = deadline 
    249 
    250    def with_deadline(self, deadline): 
    251        """Return a copy of this timeout with the given deadline. 
    252 
    253        Args: 
    254            deadline (float): The overall deadline across all invocations. 
    255 
    256        Returns: 
    257            ExponentialTimeout: A new instance with the given deadline. 
    258        """ 
    259        return ExponentialTimeout( 
    260            initial=self._initial, 
    261            maximum=self._maximum, 
    262            multiplier=self._multiplier, 
    263            deadline=deadline, 
    264        ) 
    265 
    266    def __call__(self, func): 
    267        """Apply the timeout decorator. 
    268 
    269        Args: 
    270            func (Callable): The function to apply the timeout argument to. 
    271                This function must accept a timeout keyword argument. 
    272 
    273        Returns: 
    274            Callable: The wrapped function. 
    275        """ 
    276        timeouts = _exponential_timeout_generator( 
    277            self._initial, self._maximum, self._multiplier, self._deadline 
    278        ) 
    279 
    280        @functools.wraps(func) 
    281        def func_with_timeout(*args, **kwargs): 
    282            """Wrapped function that adds timeout.""" 
    283            kwargs["timeout"] = next(timeouts) 
    284            return func(*args, **kwargs) 
    285 
    286        return func_with_timeout 
    287 
    288    def __str__(self): 
    289        return ( 
    290            "<ExponentialTimeout initial={:.1f}, maximum={:.1f}, " 
    291            "multiplier={:.1f}, deadline={:.1f}>".format( 
    292                self._initial, self._maximum, self._multiplier, self._deadline 
    293            ) 
    294        )