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 )