|
20 | 20 | from urllib.parse import urlparse |
21 | 21 |
|
22 | 22 | import urllib3 |
| 23 | +from urllib3.util.retry import Retry |
23 | 24 |
|
| 25 | +from kubernetes.utils.retry import ( |
| 26 | + is_retry_after_response, |
| 27 | + on_retry_after_error, |
| 28 | + retry_after_backoff, |
| 29 | +) |
24 | 30 | from kubernetes.client.exceptions import ApiException, ApiValueError |
25 | 31 |
|
26 | 32 | SUPPORTED_SOCKS_PROXIES = {"socks5", "socks5h", "socks4", "socks4a"} |
@@ -105,6 +111,8 @@ def getheader(self, name, default=None): |
105 | 111 | class RESTClientObject: |
106 | 112 |
|
107 | 113 | def __init__(self, configuration) -> None: |
| 114 | + self.configuration = configuration |
| 115 | + |
108 | 116 | # urllib3.PoolManager will pass all kw parameters to connectionpool |
109 | 117 | # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501 |
110 | 118 | # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501 |
@@ -217,6 +225,32 @@ def request( |
217 | 225 | read=_request_timeout[1] |
218 | 226 | ) |
219 | 227 |
|
| 228 | + client_go_read_retries = ( |
| 229 | + method in ['GET', 'HEAD'] |
| 230 | + and getattr(self.configuration, 'client_go_retries', False) |
| 231 | + ) |
| 232 | + read_retries = None |
| 233 | + if client_go_read_retries: |
| 234 | + read_retries = self._urllib3_retries_without_status( |
| 235 | + getattr(self.configuration, 'retries', None)) |
| 236 | + |
| 237 | + def read_request(check_retry_status=False): |
| 238 | + kwargs = {} |
| 239 | + if read_retries is not None: |
| 240 | + kwargs['retries'] = read_retries |
| 241 | + response = self.pool_manager.request( |
| 242 | + method, |
| 243 | + url, |
| 244 | + fields={}, |
| 245 | + timeout=timeout, |
| 246 | + headers=headers, |
| 247 | + preload_content=False, |
| 248 | + **kwargs |
| 249 | + ) |
| 250 | + if check_retry_status: |
| 251 | + self._raise_retry_after_response(response) |
| 252 | + return response |
| 253 | + |
220 | 254 | try: |
221 | 255 | # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` |
222 | 256 | if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: |
@@ -310,16 +344,67 @@ def request( |
310 | 344 | raise ApiException(status=0, reason=msg) |
311 | 345 | # For `GET`, `HEAD` |
312 | 346 | else: |
313 | | - r = self.pool_manager.request( |
314 | | - method, |
315 | | - url, |
316 | | - fields={}, |
317 | | - timeout=timeout, |
318 | | - headers=headers, |
319 | | - preload_content=False |
320 | | - ) |
| 347 | + if client_go_read_retries: |
| 348 | + backoff = retry_after_backoff( |
| 349 | + getattr(self.configuration, 'retries', None), |
| 350 | + getattr(self.configuration, 'client_go_retry_backoff', None), |
| 351 | + ) |
| 352 | + r = on_retry_after_error( |
| 353 | + backoff, self._is_read_retryable, |
| 354 | + lambda: read_request(True)) |
| 355 | + else: |
| 356 | + r = read_request() |
321 | 357 | except urllib3.exceptions.SSLError as e: |
322 | 358 | msg = "\n".join([type(e).__name__, str(e)]) |
323 | 359 | raise ApiException(status=0, reason=msg) |
324 | 360 |
|
325 | 361 | return RESTResponse(r) |
| 362 | + |
| 363 | + @classmethod |
| 364 | + def _is_read_retryable(cls, error): |
| 365 | + return is_retry_after_response(error) |
| 366 | + |
| 367 | + @staticmethod |
| 368 | + def _retry_after_error(response): |
| 369 | + error = ApiException(status=response.status, reason=response.reason) |
| 370 | + error.headers = response.getheaders() |
| 371 | + return error |
| 372 | + |
| 373 | + @classmethod |
| 374 | + def _raise_retry_after_response(cls, response): |
| 375 | + error = cls._retry_after_error(response) |
| 376 | + if not is_retry_after_response(error): |
| 377 | + return |
| 378 | + cls._read_and_close_retry_response(response) |
| 379 | + raise error |
| 380 | + |
| 381 | + @staticmethod |
| 382 | + def _read_and_close_retry_response(response): |
| 383 | + try: |
| 384 | + try: |
| 385 | + content_length = int(response.getheaders().get( |
| 386 | + 'Content-Length', '-1')) |
| 387 | + except (TypeError, ValueError): |
| 388 | + content_length = -1 |
| 389 | + if content_length <= 2 << 10: |
| 390 | + response.read(2 << 10) |
| 391 | + finally: |
| 392 | + response.close() |
| 393 | + |
| 394 | + @staticmethod |
| 395 | + def _urllib3_retries_without_status(retries): |
| 396 | + if retries is False: |
| 397 | + return False |
| 398 | + if retries is None: |
| 399 | + retries = Retry.DEFAULT |
| 400 | + elif retries is True: |
| 401 | + retries = Retry.DEFAULT |
| 402 | + elif isinstance(retries, int): |
| 403 | + retries = Retry.from_int(retries) |
| 404 | + if isinstance(retries, Retry): |
| 405 | + return retries.new( |
| 406 | + status=0, |
| 407 | + status_forcelist=(), |
| 408 | + respect_retry_after_header=False, |
| 409 | + ) |
| 410 | + return retries |
0 commit comments