2020from urllib .parse import urlparse
2121
2222import urllib3
23+ from urllib3 .util .retry import Retry
2324
2425from kubernetes .client .exceptions import ApiException , ApiValueError
26+ from kubernetes .utils .retry import (
27+ is_retry_after_response ,
28+ on_retry_after_error ,
29+ retry_after_backoff ,
30+ )
2531
2632SUPPORTED_SOCKS_PROXIES = {"socks5" , "socks5h" , "socks4" , "socks4a" }
2733RESTResponseType = urllib3 .HTTPResponse
@@ -105,6 +111,8 @@ def getheader(self, name, default=None):
105111class RESTClientObject :
106112
107113 def __init__ (self , configuration ) -> None :
114+ self .configuration = configuration
115+
108116 # urllib3.PoolManager will pass all kw parameters to connectionpool
109117 # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501
110118 # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501
@@ -217,6 +225,30 @@ def request(
217225 read = _request_timeout [1 ]
218226 )
219227
228+ client_go_retries = getattr (self .configuration , 'client_go_retries' , False )
229+ request_retries = None
230+ if client_go_retries :
231+ request_retries = self ._urllib3_retries_without_status (
232+ getattr (self .configuration , 'retries' , None ))
233+
234+ def pool_request (* args , ** kwargs ):
235+ if request_retries is not None :
236+ kwargs ['retries' ] = request_retries
237+ return self .pool_manager .request (* args , ** kwargs )
238+
239+ def read_request (check_retry_status = False ):
240+ response = pool_request (
241+ method ,
242+ url ,
243+ fields = {},
244+ timeout = timeout ,
245+ headers = headers ,
246+ preload_content = False
247+ )
248+ if check_retry_status :
249+ self ._raise_retry_after_response (response )
250+ return response
251+
220252 try :
221253 # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
222254 if method in ['POST' , 'PUT' , 'PATCH' , 'OPTIONS' , 'DELETE' ]:
@@ -247,7 +279,7 @@ def request(
247279 request_body = None
248280 if body is not None :
249281 request_body = json .dumps (body )
250- r = self . pool_manager . request (
282+ r = pool_request (
251283 method ,
252284 url ,
253285 body = request_body ,
@@ -256,7 +288,7 @@ def request(
256288 preload_content = False
257289 )
258290 elif content_type == 'application/x-www-form-urlencoded' :
259- r = self . pool_manager . request (
291+ r = pool_request (
260292 method ,
261293 url ,
262294 fields = post_params ,
@@ -272,7 +304,7 @@ def request(
272304 del headers ['Content-Type' ]
273305 # Ensures that dict objects are serialized
274306 post_params = [(a , json .dumps (b )) if isinstance (b , dict ) else (a ,b ) for a , b in post_params ]
275- r = self . pool_manager . request (
307+ r = pool_request (
276308 method ,
277309 url ,
278310 fields = post_params ,
@@ -285,7 +317,7 @@ def request(
285317 # other content types than JSON when `body` argument is
286318 # provided in serialized form.
287319 elif isinstance (body , str ) or isinstance (body , bytes ):
288- r = self . pool_manager . request (
320+ r = pool_request (
289321 method ,
290322 url ,
291323 body = body ,
@@ -295,7 +327,7 @@ def request(
295327 )
296328 elif headers ['Content-Type' ].startswith ('text/' ) and isinstance (body , bool ):
297329 request_body = "true" if body else "false"
298- r = self . pool_manager . request (
330+ r = pool_request (
299331 method ,
300332 url ,
301333 body = request_body ,
@@ -310,16 +342,57 @@ def request(
310342 raise ApiException (status = 0 , reason = msg )
311343 # For `GET`, `HEAD`
312344 else :
313- r = self .pool_manager .request (
314- method ,
315- url ,
316- fields = {},
317- timeout = timeout ,
318- headers = headers ,
319- preload_content = False
320- )
345+ if client_go_retries :
346+ backoff = retry_after_backoff (
347+ getattr (self .configuration , 'retries' , None ),
348+ getattr (self .configuration , 'client_go_retry_backoff' , None ),
349+ )
350+ r = on_retry_after_error (
351+ backoff , self ._is_read_retryable ,
352+ lambda : read_request (True ))
353+ else :
354+ r = read_request ()
321355 except urllib3 .exceptions .SSLError as e :
322356 msg = "\n " .join ([type (e ).__name__ , str (e )])
323357 raise ApiException (status = 0 , reason = msg )
324358
325359 return RESTResponse (r )
360+
361+ @classmethod
362+ def _is_read_retryable (cls , error ):
363+ return is_retry_after_response (error )
364+
365+ @staticmethod
366+ def _retry_after_error (response ):
367+ error = ApiException (status = response .status , reason = response .reason )
368+ error .headers = response .getheaders ()
369+ return error
370+
371+ @classmethod
372+ def _raise_retry_after_response (cls , response ):
373+ error = cls ._retry_after_error (response )
374+ if not is_retry_after_response (error ):
375+ return
376+ try :
377+ response .drain_conn ()
378+ except Exception :
379+ response .release_conn ()
380+ raise error
381+
382+ @staticmethod
383+ def _urllib3_retries_without_status (retries ):
384+ if retries is False :
385+ return False
386+ if retries is None :
387+ retries = Retry .DEFAULT
388+ elif retries is True :
389+ retries = Retry .DEFAULT
390+ elif isinstance (retries , int ):
391+ retries = Retry .from_int (retries )
392+ if isinstance (retries , Retry ):
393+ return retries .new (
394+ status = 0 ,
395+ status_forcelist = (),
396+ respect_retry_after_header = False ,
397+ )
398+ return retries
0 commit comments