|
| 1 | +# Copyright 2017 Google Inc. |
| 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 | +
|
| 17 | +These decorators are used to wrap API methods to apply either a constant |
| 18 | +or exponential timeout argument. |
| 19 | +
|
| 20 | +For example, imagine an API method that can take a while to return results, |
| 21 | +such as one that might block until a resource is ready: |
| 22 | +
|
| 23 | +.. code-block:: python |
| 24 | +
|
| 25 | + def is_thing_ready(timeout=None): |
| 26 | + response = requests.get('https://example.com/is_thing_ready') |
| 27 | + response.raise_for_status() |
| 28 | + return response.json() |
| 29 | +
|
| 30 | +This module allows a function like this to be wrapped so that timeouts are |
| 31 | +automatically determined, for example: |
| 32 | +
|
| 33 | +.. code-block:: python |
| 34 | +
|
| 35 | + timeout_ = timeout.ExponentialTimeout() |
| 36 | + is_thing_ready_with_timeout = timeout_(is_thing_ready) |
| 37 | +
|
| 38 | + for n in range(10): |
| 39 | + try: |
| 40 | + is_thing_ready_with_timeout({'example': 'data'}) |
| 41 | + except: |
| 42 | + pass |
| 43 | +
|
| 44 | +In this example the first call to ``is_thing_ready`` will have a relatively |
| 45 | +small timeout (like 1 second). If the resource is available and the request |
| 46 | +completes quickly, the loop exits. But, if the resource isn't yet available |
| 47 | +and the request times out, it'll be retried - this time with a larger timeout. |
| 48 | +
|
| 49 | +In the broader context these decorators are typically combined with |
| 50 | +:mod:`google.api.core.retry` to implement API methods with a signature that |
| 51 | +matches ``api_method(request, timeout=None, retry=None)``. |
| 52 | +""" |
| 53 | + |
| 54 | +from __future__ import unicode_literals |
| 55 | + |
| 56 | +import datetime |
| 57 | + |
| 58 | +import six |
| 59 | + |
| 60 | +from google.api.core.helpers 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 | + |
| 70 | +@six.python_2_unicode_compatible |
| 71 | +class ConstantTimeout(object): |
| 72 | + """A decorator that adds a constant timeout argument. |
| 73 | +
|
| 74 | + This is effectively equivalent to |
| 75 | + ``functools.partial(func, timeout=timeout)``. |
| 76 | +
|
| 77 | + Args: |
| 78 | + timeout (Optional[float]): the timeout (in seconds) to applied to the |
| 79 | + wrapped function. If `None`, the target function is expected to |
| 80 | + never timeout. |
| 81 | + """ |
| 82 | + def __init__(self, timeout=None): |
| 83 | + self._timeout = timeout |
| 84 | + |
| 85 | + def __call__(self, func): |
| 86 | + """Apply the timeout decorator. |
| 87 | +
|
| 88 | + Args: |
| 89 | + func (Callable): The function to apply the timeout argument to. |
| 90 | + This function must accept a timeout keyword argument. |
| 91 | +
|
| 92 | + Returns: |
| 93 | + Callable: The wrapped function. |
| 94 | + """ |
| 95 | + @six.wraps(func) |
| 96 | + def func_with_timeout(*args, **kwargs): |
| 97 | + """Wrapped function that adds timeout.""" |
| 98 | + kwargs['timeout'] = self._timeout |
| 99 | + return func(*args, **kwargs) |
| 100 | + return func_with_timeout |
| 101 | + |
| 102 | + def __str__(self): |
| 103 | + return '<ConstantTimeout timeout={:.1f}>'.format(self._timeout) |
| 104 | + |
| 105 | + |
| 106 | +def _exponential_timeout_generator(initial, maximum, multiplier, deadline): |
| 107 | + """A generator that yields exponential timeout values. |
| 108 | +
|
| 109 | + Args: |
| 110 | + initial (float): The initial timeout. |
| 111 | + maximum (float): The maximum timeout. |
| 112 | + multiplier (float): The multiplier applied to the timeout. |
| 113 | + deadline (float): The overall deadline across all invocations. |
| 114 | +
|
| 115 | + Yields: |
| 116 | + float: A timeout value. |
| 117 | + """ |
| 118 | + if deadline is not None: |
| 119 | + deadline_datetime = ( |
| 120 | + datetime_helpers.utcnow() + |
| 121 | + datetime.timedelta(seconds=deadline)) |
| 122 | + else: |
| 123 | + deadline_datetime = datetime.datetime.max |
| 124 | + |
| 125 | + timeout = initial |
| 126 | + while True: |
| 127 | + now = datetime_helpers.utcnow() |
| 128 | + yield min( |
| 129 | + # The calculated timeout based on invocations. |
| 130 | + timeout, |
| 131 | + # The set maximum timeout. |
| 132 | + maximum, |
| 133 | + # The remaining time before the deadline is reached. |
| 134 | + float((deadline_datetime - now).seconds)) |
| 135 | + timeout = timeout * multiplier |
| 136 | + |
| 137 | + |
| 138 | +@six.python_2_unicode_compatible |
| 139 | +class ExponentialTimeout(object): |
| 140 | + """A decorator that adds an exponentially increasing timeout argument. |
| 141 | +
|
| 142 | + This is useful if a function is called multiple times. Each time the |
| 143 | + function is called this decorator will calculate a new timeout parameter |
| 144 | + based on the the number of times the function has been called. |
| 145 | +
|
| 146 | + For example |
| 147 | +
|
| 148 | + .. code-block:: python |
| 149 | +
|
| 150 | + Args: |
| 151 | + initial (float): The initial timeout to pass. |
| 152 | + maximum (float): The maximum timeout for any one call. |
| 153 | + multiplier (float): The multiplier applied to the timeout for each |
| 154 | + invocation. |
| 155 | + deadline (Optional[float]): The overall deadline across all |
| 156 | + invocations. This is used to prevent a very large calculated |
| 157 | + timeout from pushing the overall execution time over the deadline. |
| 158 | + This is especially useful in conjuction with |
| 159 | + :mod:`google.api.core.retry`. If ``None``, the timeouts will not |
| 160 | + be adjusted to accomodate an overall deadline. |
| 161 | + """ |
| 162 | + def __init__( |
| 163 | + self, |
| 164 | + initial=_DEFAULT_INITIAL_TIMEOUT, |
| 165 | + maximum=_DEFAULT_MAXIMUM_TIMEOUT, |
| 166 | + multiplier=_DEFAULT_TIMEOUT_MULTIPLIER, |
| 167 | + deadline=_DEFAULT_DEADLINE): |
| 168 | + self._initial = initial |
| 169 | + self._maximum = maximum |
| 170 | + self._multiplier = multiplier |
| 171 | + self._deadline = deadline |
| 172 | + |
| 173 | + def with_deadline(self, deadline): |
| 174 | + """Return a copy of this teimout with the given deadline. |
| 175 | +
|
| 176 | + Args: |
| 177 | + deadline (float): The overall deadline across all invocations. |
| 178 | +
|
| 179 | + Returns: |
| 180 | + ExponentialTimeout: A new instance with the given deadline. |
| 181 | + """ |
| 182 | + return ExponentialTimeout( |
| 183 | + initial=self._initial, |
| 184 | + maximum=self._maximum, |
| 185 | + multiplier=self._multiplier, |
| 186 | + deadline=deadline) |
| 187 | + |
| 188 | + def __call__(self, func): |
| 189 | + """Apply the timeout decorator. |
| 190 | +
|
| 191 | + Args: |
| 192 | + func (Callable): The function to apply the timeout argument to. |
| 193 | + This function must accept a timeout keyword argument. |
| 194 | +
|
| 195 | + Returns: |
| 196 | + Callable: The wrapped function. |
| 197 | + """ |
| 198 | + timeouts = _exponential_timeout_generator( |
| 199 | + self._initial, self._maximum, self._multiplier, self._deadline) |
| 200 | + |
| 201 | + @six.wraps(func) |
| 202 | + def func_with_timeout(*args, **kwargs): |
| 203 | + """Wrapped function that adds timeout.""" |
| 204 | + kwargs['timeout'] = next(timeouts) |
| 205 | + return func(*args, **kwargs) |
| 206 | + |
| 207 | + return func_with_timeout |
| 208 | + |
| 209 | + def __str__(self): |
| 210 | + return ( |
| 211 | + '<ExponentialTimeout initial={:.1f}, maximum={:.1f}, ' |
| 212 | + 'multiplier={:.1f}, deadline={:.1f}>'.format( |
| 213 | + self._initial, self._maximum, self._multiplier, |
| 214 | + self._deadline)) |
0 commit comments