2
2
3
3
import json
4
4
import logging
5
- import time
6
5
from typing import BinaryIO , Tuple
7
6
8
7
import pkg_resources
9
8
import requests
9
+ from requests .adapters import HTTPAdapter , Retry
10
10
11
11
from linode_api4 .errors import ApiError , UnexpectedResponseError
12
12
from linode_api4 .groups import *
22
22
logger = logging .getLogger (__name__ )
23
23
24
24
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
+
25
35
class LinodeClient :
26
36
def __init__ (
27
37
self ,
28
38
token ,
29
39
base_url = "https://api.linode.com/v4" ,
30
40
user_agent = None ,
31
41
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 ,
33
46
):
34
47
"""
35
48
The main interface to the Linode API.
@@ -51,26 +64,57 @@ def __init__(
51
64
can be found in the API docs, but at time of writing
52
65
are between 25 and 500.
53
66
: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.
58
80
"""
59
81
self .base_url = base_url
60
82
self ._add_user_agent = user_agent
61
83
self .token = token
62
- self .session = requests .Session ()
63
84
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 )
65
90
66
91
# 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 )
74
118
75
119
#: Access methods related to Linodes - see :any:`LinodeGroup` for
76
120
#: more information
@@ -196,29 +240,11 @@ def _api_call(
196
240
if data is not None :
197
241
body = json .dumps (data )
198
242
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 ))
222
248
223
249
if 399 < response .status_code < 600 :
224
250
j = None
@@ -288,6 +314,29 @@ def put(self, *args, **kwargs):
288
314
def delete (self , * args , ** kwargs ):
289
315
return self ._api_call (* args , method = self .session .delete , ** kwargs )
290
316
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
+
291
340
def image_create (self , disk , label = None , description = None ):
292
341
"""
293
342
.. note:: This method is an alias to maintain backwards compatibility.
0 commit comments