Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google/api_core/timeout.py: 34%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

68 statements  

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 )