Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google/api_core/retry/retry_base.py: 44%

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

88 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"""Shared classes and functions for retrying requests. 

16 

17:class:`_BaseRetry` is the base class for :class:`Retry`, 

18:class:`AsyncRetry`, :class:`StreamingRetry`, and :class:`AsyncStreamingRetry`. 

19""" 

20 

21from __future__ import annotations 

22 

23import logging 

24import random 

25import time 

26from enum import Enum 

27from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional 

28 

29import requests.exceptions 

30from google.auth import exceptions as auth_exceptions 

31 

32from google.api_core import exceptions 

33 

34if TYPE_CHECKING: 

35 import sys 

36 

37 if sys.version_info >= (3, 11): 

38 from typing import Self 

39 else: 

40 from typing_extensions import Self 

41 

42_DEFAULT_INITIAL_DELAY = 1.0 # seconds 

43_DEFAULT_MAXIMUM_DELAY = 60.0 # seconds 

44_DEFAULT_DELAY_MULTIPLIER = 2.0 

45_DEFAULT_DEADLINE = 60.0 * 2.0 # seconds 

46 

47_LOGGER = logging.getLogger("google.api_core.retry") 

48 

49 

50def if_exception_type( 

51 *exception_types: type[Exception], 

52) -> Callable[[Exception], bool]: 

53 """Creates a predicate to check if the exception is of a given type. 

54 

55 Args: 

56 exception_types (Sequence[:func:`type`]): The exception types to check 

57 for. 

58 

59 Returns: 

60 Callable[Exception]: A predicate that returns True if the provided 

61 exception is of the given type(s). 

62 """ 

63 

64 def if_exception_type_predicate(exception: Exception) -> bool: 

65 """Bound predicate for checking an exception type.""" 

66 return isinstance(exception, exception_types) 

67 

68 return if_exception_type_predicate 

69 

70 

71# pylint: disable=invalid-name 

72# Pylint sees this as a constant, but it is also an alias that should be 

73# considered a function. 

74if_transient_error = if_exception_type( 

75 exceptions.InternalServerError, 

76 exceptions.TooManyRequests, 

77 exceptions.ServiceUnavailable, 

78 requests.exceptions.ConnectionError, 

79 requests.exceptions.ChunkedEncodingError, 

80 auth_exceptions.TransportError, 

81) 

82"""A predicate that checks if an exception is a transient API error. 

83 

84The following server errors are considered transient: 

85 

86- :class:`google.api_core.exceptions.InternalServerError` - HTTP 500, gRPC 

87 ``INTERNAL(13)`` and its subclasses. 

88- :class:`google.api_core.exceptions.TooManyRequests` - HTTP 429 

89- :class:`google.api_core.exceptions.ServiceUnavailable` - HTTP 503 

90- :class:`requests.exceptions.ConnectionError` 

91- :class:`requests.exceptions.ChunkedEncodingError` - The server declared 

92 chunked encoding but sent an invalid chunk. 

93- :class:`google.auth.exceptions.TransportError` - Used to indicate an 

94 error occurred during an HTTP request. 

95""" 

96# pylint: enable=invalid-name 

97 

98 

99def exponential_sleep_generator( 

100 initial: float, maximum: float, multiplier: float = _DEFAULT_DELAY_MULTIPLIER 

101): 

102 """Generates sleep intervals based on the exponential back-off algorithm. 

103 

104 This implements the `Truncated Exponential Back-off`_ algorithm. 

105 

106 .. _Truncated Exponential Back-off: 

107 https://cloud.google.com/storage/docs/exponential-backoff 

108 

109 Args: 

110 initial (float): The minimum amount of time to delay. This must 

111 be greater than 0. 

112 maximum (float): The maximum amount of time to delay. 

113 multiplier (float): The multiplier applied to the delay. 

114 

115 Yields: 

116 float: successive sleep intervals. 

117 """ 

118 max_delay = min(initial, maximum) 

119 while True: 

120 yield random.uniform(0.0, max_delay) 

121 max_delay = min(max_delay * multiplier, maximum) 

122 

123 

124class RetryFailureReason(Enum): 

125 """ 

126 The cause of a failed retry, used when building exceptions 

127 """ 

128 

129 TIMEOUT = 0 

130 NON_RETRYABLE_ERROR = 1 

131 

132 

133def build_retry_error( 

134 exc_list: list[Exception], 

135 reason: RetryFailureReason, 

136 timeout_val: float | None, 

137 **kwargs: Any, 

138) -> tuple[Exception, Exception | None]: 

139 """ 

140 Default exception_factory implementation. 

141 

142 Returns a RetryError if the failure is due to a timeout, otherwise 

143 returns the last exception encountered. 

144 

145 Args: 

146 - exc_list: list of exceptions that occurred during the retry 

147 - reason: reason for the retry failure. 

148 Can be TIMEOUT or NON_RETRYABLE_ERROR 

149 - timeout_val: the original timeout value for the retry (in seconds), for use in the exception message 

150 

151 Returns: 

152 - tuple: a tuple of the exception to be raised, and the cause exception if any 

153 """ 

154 if reason == RetryFailureReason.TIMEOUT: 

155 # return RetryError with the most recent exception as the cause 

156 src_exc = exc_list[-1] if exc_list else None 

157 timeout_val_str = f"of {timeout_val:0.1f}s " if timeout_val is not None else "" 

158 return ( 

159 exceptions.RetryError( 

160 f"Timeout {timeout_val_str}exceeded", 

161 src_exc, 

162 ), 

163 src_exc, 

164 ) 

165 elif exc_list: 

166 # return most recent exception encountered and its cause 

167 final_exc = exc_list[-1] 

168 cause = getattr(final_exc, "__cause__", None) 

169 return final_exc, cause 

170 else: 

171 # no exceptions were given in exc_list. Raise generic RetryError 

172 return exceptions.RetryError("Unknown error", None), None 

173 

174 

175def _retry_error_helper( 

176 exc: Exception, 

177 deadline: float | None, 

178 sleep_iterator: Iterator[float], 

179 error_list: list[Exception], 

180 predicate_fn: Callable[[Exception], bool], 

181 on_error_fn: Callable[[Exception], None] | None, 

182 exc_factory_fn: Callable[ 

183 [list[Exception], RetryFailureReason, float | None], 

184 tuple[Exception, Exception | None], 

185 ], 

186 original_timeout: float | None, 

187) -> float: 

188 """ 

189 Shared logic for handling an error for all retry implementations 

190 

191 - Raises an error on timeout or non-retryable error 

192 - Calls on_error_fn if provided 

193 - Logs the error 

194 

195 Args: 

196 - exc: the exception that was raised 

197 - deadline: the deadline for the retry, calculated as a diff from time.monotonic() 

198 - sleep_iterator: iterator to draw the next backoff value from 

199 - error_list: the list of exceptions that have been raised so far 

200 - predicate_fn: takes `exc` and returns true if the operation should be retried 

201 - on_error_fn: callback to execute when a retryable error occurs 

202 - exc_factory_fn: callback used to build the exception to be raised on terminal failure 

203 - original_timeout_val: the original timeout value for the retry (in seconds), 

204 to be passed to the exception factory for building an error message 

205 Returns: 

206 - the sleep value chosen before the next attempt 

207 """ 

208 error_list.append(exc) 

209 if not predicate_fn(exc): 

210 final_exc, source_exc = exc_factory_fn( 

211 error_list, 

212 RetryFailureReason.NON_RETRYABLE_ERROR, 

213 original_timeout, 

214 ) 

215 raise final_exc from source_exc 

216 if on_error_fn is not None: 

217 on_error_fn(exc) 

218 # next_sleep is fetched after the on_error callback, to allow clients 

219 # to update sleep_iterator values dynamically in response to errors 

220 try: 

221 next_sleep = next(sleep_iterator) 

222 except StopIteration: 

223 raise ValueError("Sleep generator stopped yielding sleep values.") from exc 

224 if deadline is not None and time.monotonic() + next_sleep > deadline: 

225 final_exc, source_exc = exc_factory_fn( 

226 error_list, 

227 RetryFailureReason.TIMEOUT, 

228 original_timeout, 

229 ) 

230 raise final_exc from source_exc 

231 _LOGGER.debug( 

232 "Retrying due to {}, sleeping {:.1f}s ...".format(error_list[-1], next_sleep) 

233 ) 

234 return next_sleep 

235 

236 

237class _BaseRetry(object): 

238 """ 

239 Base class for retry configuration objects. This class is intended to capture retry 

240 and backoff configuration that is common to both synchronous and asynchronous retries, 

241 for both unary and streaming RPCs. It is not intended to be instantiated directly, 

242 but rather to be subclassed by the various retry configuration classes. 

243 """ 

244 

245 def __init__( 

246 self, 

247 predicate: Callable[[Exception], bool] = if_transient_error, 

248 initial: float = _DEFAULT_INITIAL_DELAY, 

249 maximum: float = _DEFAULT_MAXIMUM_DELAY, 

250 multiplier: float = _DEFAULT_DELAY_MULTIPLIER, 

251 timeout: Optional[float] = _DEFAULT_DEADLINE, 

252 on_error: Optional[Callable[[Exception], Any]] = None, 

253 **kwargs: Any, 

254 ) -> None: 

255 self._predicate = predicate 

256 self._initial = initial 

257 self._multiplier = multiplier 

258 self._maximum = maximum 

259 self._timeout = kwargs.get("deadline", timeout) 

260 self._deadline = self._timeout 

261 self._on_error = on_error 

262 

263 def __call__(self, *args, **kwargs) -> Any: 

264 raise NotImplementedError("Not implemented in base class") 

265 

266 @property 

267 def deadline(self) -> float | None: 

268 """ 

269 DEPRECATED: use ``timeout`` instead. Refer to the ``Retry`` class 

270 documentation for details. 

271 """ 

272 return self._timeout 

273 

274 @property 

275 def timeout(self) -> float | None: 

276 return self._timeout 

277 

278 def with_deadline(self, deadline: float | None) -> Self: 

279 """Return a copy of this retry with the given timeout. 

280 

281 DEPRECATED: use :meth:`with_timeout` instead. Refer to the ``Retry`` class 

282 documentation for details. 

283 

284 Args: 

285 deadline (float|None): How long to keep retrying, in seconds. If None, 

286 no timeout is enforced. 

287 

288 Returns: 

289 Retry: A new retry instance with the given timeout. 

290 """ 

291 return self.with_timeout(deadline) 

292 

293 def with_timeout(self, timeout: float | None) -> Self: 

294 """Return a copy of this retry with the given timeout. 

295 

296 Args: 

297 timeout (float): How long to keep retrying, in seconds. If None, 

298 no timeout will be enforced. 

299 

300 Returns: 

301 Retry: A new retry instance with the given timeout. 

302 """ 

303 return type(self)( 

304 predicate=self._predicate, 

305 initial=self._initial, 

306 maximum=self._maximum, 

307 multiplier=self._multiplier, 

308 timeout=timeout, 

309 on_error=self._on_error, 

310 ) 

311 

312 def with_predicate(self, predicate: Callable[[Exception], bool]) -> Self: 

313 """Return a copy of this retry with the given predicate. 

314 

315 Args: 

316 predicate (Callable[Exception]): A callable that should return 

317 ``True`` if the given exception is retryable. 

318 

319 Returns: 

320 Retry: A new retry instance with the given predicate. 

321 """ 

322 return type(self)( 

323 predicate=predicate, 

324 initial=self._initial, 

325 maximum=self._maximum, 

326 multiplier=self._multiplier, 

327 timeout=self._timeout, 

328 on_error=self._on_error, 

329 ) 

330 

331 def with_delay( 

332 self, 

333 initial: Optional[float] = None, 

334 maximum: Optional[float] = None, 

335 multiplier: Optional[float] = None, 

336 ) -> Self: 

337 """Return a copy of this retry with the given delay options. 

338 

339 Args: 

340 initial (float): The minimum amount of time to delay (in seconds). This must 

341 be greater than 0. If None, the current value is used. 

342 maximum (float): The maximum amount of time to delay (in seconds). If None, the 

343 current value is used. 

344 multiplier (float): The multiplier applied to the delay. If None, the current 

345 value is used. 

346 

347 Returns: 

348 Retry: A new retry instance with the given delay options. 

349 """ 

350 return type(self)( 

351 predicate=self._predicate, 

352 initial=initial if initial is not None else self._initial, 

353 maximum=maximum if maximum is not None else self._maximum, 

354 multiplier=multiplier if multiplier is not None else self._multiplier, 

355 timeout=self._timeout, 

356 on_error=self._on_error, 

357 ) 

358 

359 def __str__(self) -> str: 

360 return ( 

361 "<{} predicate={}, initial={:.1f}, maximum={:.1f}, " 

362 "multiplier={:.1f}, timeout={}, on_error={}>".format( 

363 type(self).__name__, 

364 self._predicate, 

365 self._initial, 

366 self._maximum, 

367 self._multiplier, 

368 self._timeout, # timeout can be None, thus no {:.1f} 

369 self._on_error, 

370 ) 

371 )