22
33import json
44import logging
5- import time
65from typing import BinaryIO , Tuple
76
87import pkg_resources
98import requests
9+ from requests .adapters import HTTPAdapter , Retry
1010
1111from linode_api4 .errors import ApiError , UnexpectedResponseError
1212from linode_api4 .groups import *
2222logger = logging .getLogger (__name__ )
2323
2424
25+ class LinearRetry (Retry ):
26+ """
27+ Linear retry is a subclass of Retry that uses a linear backoff strategy.
28+ This is necessary to maintain backwards compatibility with the old retry system.
29+ """
30+
31+ def get_backoff_time (self ):
32+ return self .backoff_factor
33+
34+
2535class LinodeClient :
2636 def __init__ (
2737 self ,
2838 token ,
2939 base_url = "https://api.linode.com/v4" ,
3040 user_agent = None ,
3141 page_size = None ,
32- retry_rate_limit_interval = None ,
42+ retry = True ,
43+ retry_rate_limit_interval = 1.0 ,
44+ retry_max = 5 ,
45+ retry_statuses = None ,
3346 ):
3447 """
3548 The main interface to the Linode API.
@@ -51,26 +64,57 @@ def __init__(
5164 can be found in the API docs, but at time of writing
5265 are between 25 and 500.
5366 :type page_size: int
54- :param retry_rate_limit_interval: If given, 429 responses will be automatically
55- retried up to 5 times with the given interval,
56- in seconds, between attempts.
57- :type retry_rate_limit_interval: int
67+ :param retry: Whether API requests should automatically be retries on known
68+ intermittent responses.
69+ :type retry: bool
70+ :param retry_rate_limit_interval: The amount of time to wait between HTTP request
71+ retries.
72+ :type retry_rate_limit_interval: float
73+ :param retry_max: The number of request retries that should be attempted before
74+ raising an API error.
75+ :type retry_max: int
76+ :type retry_statuses: List of int
77+ :param retry_statuses: Additional HTTP response statuses to retry on.
78+ By default, the client will retry on 408, 429, and 502
79+ responses.
5880 """
5981 self .base_url = base_url
6082 self ._add_user_agent = user_agent
6183 self .token = token
62- self .session = requests .Session ()
6384 self .page_size = page_size
64- self .retry_rate_limit_interval = retry_rate_limit_interval
85+
86+ retry_forcelist = [408 , 429 , 502 ]
87+
88+ if retry_statuses is not None :
89+ retry_forcelist .extend (retry_statuses )
6590
6691 # make sure we got a sane backoff
67- if self .retry_rate_limit_interval is not None :
68- if not isinstance (self .retry_rate_limit_interval , int ):
69- raise ValueError ("retry_rate_limit_interval must be an int" )
70- if self .retry_rate_limit_interval < 1 :
71- raise ValueError (
72- "retry_rate_limit_interval must not be less than 1"
73- )
92+ if not isinstance (retry_rate_limit_interval , float ):
93+ raise ValueError ("retry_rate_limit_interval must be a float" )
94+
95+ # Ensure the max retries value is valid
96+ if not isinstance (retry_max , int ):
97+ raise ValueError ("retry_max must be an int" )
98+
99+ self .retry = retry
100+ self .retry_rate_limit_interval = retry_rate_limit_interval
101+ self .retry_max = retry_max
102+ self .retry_statuses = retry_statuses
103+
104+ # Initialize the HTTP client session
105+ self .session = requests .Session ()
106+
107+ self ._retry_config = LinearRetry (
108+ total = retry_max if retry else 0 ,
109+ status_forcelist = retry_forcelist ,
110+ respect_retry_after_header = True ,
111+ backoff_factor = retry_rate_limit_interval ,
112+ raise_on_status = False ,
113+ )
114+ retry_adapter = HTTPAdapter (max_retries = self ._retry_config )
115+
116+ self .session .mount ("http://" , retry_adapter )
117+ self .session .mount ("https://" , retry_adapter )
74118
75119 #: Access methods related to Linodes - see :any:`LinodeGroup` for
76120 #: more information
@@ -196,29 +240,11 @@ def _api_call(
196240 if data is not None :
197241 body = json .dumps (data )
198242
199- # retry on 429 response
200- max_retries = 5 if self .retry_rate_limit_interval else 1
201- for attempt in range (max_retries ):
202- response = method (url , headers = headers , data = body )
203-
204- warning = response .headers .get ("Warning" , None )
205- if warning :
206- logger .warning (
207- "Received warning from server: {}" .format (warning )
208- )
209-
210- # if we were configured to retry 429s, and we got a 429, sleep briefly and then retry
211- if self .retry_rate_limit_interval and response .status_code == 429 :
212- logger .warning (
213- "Received 429 response; waiting {} seconds and retrying request (attempt {}/{})" .format (
214- self .retry_rate_limit_interval ,
215- attempt ,
216- max_retries ,
217- )
218- )
219- time .sleep (self .retry_rate_limit_interval )
220- else :
221- break
243+ response = method (url , headers = headers , data = body )
244+
245+ warning = response .headers .get ("Warning" , None )
246+ if warning :
247+ logger .warning ("Received warning from server: {}" .format (warning ))
222248
223249 if 399 < response .status_code < 600 :
224250 j = None
@@ -288,6 +314,29 @@ def put(self, *args, **kwargs):
288314 def delete (self , * args , ** kwargs ):
289315 return self ._api_call (* args , method = self .session .delete , ** kwargs )
290316
317+ def __setattr__ (self , key , value ):
318+ # Allow for dynamic updating of the retry config
319+ handlers = {
320+ "retry_rate_limit_interval" : lambda : setattr (
321+ self ._retry_config , "backoff_factor" , value
322+ ),
323+ "retry" : lambda : setattr (
324+ self ._retry_config , "total" , self .retry_max if value else 0
325+ ),
326+ "retry_max" : lambda : setattr (
327+ self ._retry_config , "total" , value if self .retry else 0
328+ ),
329+ "retry_statuses" : lambda : setattr (
330+ self ._retry_config , "status_forcelist" , value
331+ ),
332+ }
333+
334+ handler = handlers .get (key )
335+ if hasattr (self , "_retry_config" ) and handler is not None :
336+ handler ()
337+
338+ super ().__setattr__ (key , value )
339+
291340 def image_create (self , disk , label = None , description = None ):
292341 """
293342 .. note:: This method is an alias to maintain backwards compatibility.
0 commit comments