1
1
# coding=utf-8
2
2
import logging
3
+ import random
3
4
from json import dumps
4
5
5
6
import requests
9
10
from oauthlib .oauth1 .rfc5849 import SIGNATURE_RSA_SHA512 as SIGNATURE_RSA
10
11
except ImportError :
11
12
from oauthlib .oauth1 import SIGNATURE_RSA
13
+ import time
14
+
15
+ import urllib3
12
16
from requests import HTTPError
13
17
from requests_oauthlib import OAuth1 , OAuth2
14
18
from six .moves .urllib .parse import urlencode
15
- import time
16
19
from urllib3 .util import Retry
17
- import urllib3
18
20
19
21
from atlassian .request_utils import get_default_logger
20
22
@@ -69,6 +71,9 @@ def __init__(
69
71
retry_status_codes = [413 , 429 , 503 ],
70
72
max_backoff_seconds = 1800 ,
71
73
max_backoff_retries = 1000 ,
74
+ backoff_factor = 1.0 ,
75
+ backoff_jitter = 1.0 ,
76
+ retry_with_header = True ,
72
77
):
73
78
"""
74
79
init function for the AtlassianRestAPI object.
@@ -102,6 +107,19 @@ def __init__(
102
107
wait any longer than this. Defaults to 1800.
103
108
:param max_backoff_retries: Maximum number of retries to try before
104
109
continuing. Defaults to 1000.
110
+ :param backoff_factor: Factor by which to multiply the backoff time (for exponential backoff).
111
+ Defaults to 1.0.
112
+ :param backoff_jitter: Random variation to add to the backoff time to avoid synchronized retries.
113
+ Defaults to 1.0.
114
+ :param retry_with_header: Enable retry logic based on the `Retry-After` header.
115
+ If set to True, the request will automatically retry if the response
116
+ contains a `Retry-After` header with a delay and has a status code of 429. The retry delay will be extracted
117
+ from the `Retry-After` header and the request will be paused for the specified
118
+ duration before retrying. Defaults to True.
119
+ If the `Retry-After` header is not present, retries will not occur.
120
+ However, if the `Retry-After` header is missing and `backoff_and_retry` is enabled,
121
+ the retry logic will still be triggered based on the status code 429,
122
+ provided that 429 is included in the `retry_status_codes` list.
105
123
"""
106
124
self .url = url
107
125
self .username = username
@@ -115,6 +133,14 @@ def __init__(
115
133
self .cloud = cloud
116
134
self .proxies = proxies
117
135
self .cert = cert
136
+ self .backoff_and_retry = backoff_and_retry
137
+ self .max_backoff_retries = max_backoff_retries
138
+ self .retry_status_codes = retry_status_codes
139
+ self .max_backoff_seconds = max_backoff_seconds
140
+ self .use_urllib3_retry = int (urllib3 .__version__ .split ("." )[0 ]) >= 2
141
+ self .backoff_factor = backoff_factor
142
+ self .backoff_jitter = backoff_jitter
143
+ self .retry_with_header = retry_with_header
118
144
if session is None :
119
145
self ._session = requests .Session ()
120
146
else :
@@ -123,17 +149,17 @@ def __init__(
123
149
if proxies is not None :
124
150
self ._session .proxies = self .proxies
125
151
126
- if backoff_and_retry and int ( urllib3 . __version__ . split ( "." )[ 0 ]) >= 2 :
152
+ if self . backoff_and_retry and self . use_urllib3_retry :
127
153
# Note: we only retry on status and not on any of the
128
154
# other supported reasons
129
155
retries = Retry (
130
156
total = None ,
131
- status = max_backoff_retries ,
157
+ status = self . max_backoff_retries ,
132
158
allowed_methods = None ,
133
- status_forcelist = retry_status_codes ,
134
- backoff_factor = 1 ,
135
- backoff_jitter = 1 ,
136
- backoff_max = max_backoff_seconds ,
159
+ status_forcelist = self . retry_status_codes ,
160
+ backoff_factor = self . backoff_factor ,
161
+ backoff_jitter = self . backoff_jitter ,
162
+ backoff_max = self . max_backoff_seconds ,
137
163
)
138
164
self ._session .mount (self .url , HTTPAdapter (max_retries = retries ))
139
165
if username and password :
@@ -209,6 +235,57 @@ def _response_handler(response):
209
235
log .error (e )
210
236
return None
211
237
238
+ def _calculate_backoff_value (self , retry_count ):
239
+ """
240
+ Calculate the backoff delay for a given retry attempt.
241
+
242
+ This method computes an exponential backoff value based on the retry count.
243
+ Optionally, it adds a random jitter to introduce variability in the delay
244
+ to prevent synchronized retries in distributed systems. The backoff value is
245
+ clamped between 0 and a maximum allowed delay (`self.max_backoff_seconds`).
246
+
247
+ :param retry_count: int, REQUIRED: The current retry attempt number (1-based).
248
+ Determines the exponential backoff delay.
249
+ :return: float: The calculated backoff delay in seconds, adjusted for jitter
250
+ and clamped to the maximum allowable value.
251
+ """
252
+ backoff_value = 2 ** (retry_count - 1 )
253
+ if self .backoff_jitter != 0.0 :
254
+ backoff_value += random .random () * self .backoff_jitter
255
+ return float (max (0 , min (self .max_backoff_seconds , backoff_value )))
256
+
257
+ def _retry_handler (self ):
258
+ """
259
+ Creates and returns a retry handler function for managing HTTP request retries.
260
+
261
+ The returned handler function determines whether a request should be retried
262
+ based on the response and retry settings.
263
+
264
+ :return: Callable[[Response], bool]: A function that takes an HTTP response object as input and
265
+ returns `True` if the request should be retried, or `False` otherwise.
266
+ """
267
+ retries = 0
268
+
269
+ def _handle (response ):
270
+ nonlocal retries
271
+
272
+ if self .retry_with_header and "Retry-After" in response .headers and response .status_code == 429 :
273
+ time .sleep (int (response .headers ["Retry-After" ]))
274
+ return True
275
+
276
+ if not self .backoff_and_retry or self .use_urllib3_retry :
277
+ return False
278
+
279
+ if retries < self .max_backoff_retries and response .status_code in self .retry_status_codes :
280
+ retries += 1
281
+ backoff_value = self ._calculate_backoff_value (retries )
282
+ time .sleep (backoff_value )
283
+ return True
284
+
285
+ return False
286
+
287
+ return _handle
288
+
212
289
def log_curl_debug (self , method , url , data = None , headers = None , level = logging .DEBUG ):
213
290
"""
214
291
@@ -274,30 +351,32 @@ def request(
274
351
:param advanced_mode: bool, OPTIONAL: Return the raw response
275
352
:return:
276
353
"""
354
+ url = self .url_joiner (None if absolute else self .url , path , trailing )
355
+ params_already_in_url = True if "?" in url else False
356
+ if params or flags :
357
+ if params_already_in_url :
358
+ url += "&"
359
+ else :
360
+ url += "?"
361
+ if params :
362
+ url += urlencode (params or {})
363
+ if flags :
364
+ url += ("&" if params or params_already_in_url else "" ) + "&" .join (flags or [])
365
+ json_dump = None
366
+ if files is None :
367
+ data = None if not data else dumps (data )
368
+ json_dump = None if not json else dumps (json )
369
+
370
+ headers = headers or self .default_headers
277
371
372
+ retry_handler = self ._retry_handler ()
278
373
while True :
279
- url = self .url_joiner (None if absolute else self .url , path , trailing )
280
- params_already_in_url = True if "?" in url else False
281
- if params or flags :
282
- if params_already_in_url :
283
- url += "&"
284
- else :
285
- url += "?"
286
- if params :
287
- url += urlencode (params or {})
288
- if flags :
289
- url += ("&" if params or params_already_in_url else "" ) + "&" .join (flags or [])
290
- json_dump = None
291
- if files is None :
292
- data = None if not data else dumps (data )
293
- json_dump = None if not json else dumps (json )
294
374
self .log_curl_debug (
295
375
method = method ,
296
376
url = url ,
297
377
headers = headers ,
298
- data = data if data else json_dump ,
378
+ data = data or json_dump ,
299
379
)
300
- headers = headers or self .default_headers
301
380
response = self ._session .request (
302
381
method = method ,
303
382
url = url ,
@@ -310,15 +389,15 @@ def request(
310
389
proxies = self .proxies ,
311
390
cert = self .cert ,
312
391
)
313
- response .encoding = "utf-8"
392
+ continue_retries = retry_handler (response )
393
+ if continue_retries :
394
+ continue
395
+ break
314
396
315
- log .debug ("HTTP: %s %s -> %s %s" , method , path , response .status_code , response .reason )
316
- log .debug ("HTTP: Response text -> %s" , response .text )
397
+ response .encoding = "utf-8"
317
398
318
- if response .status_code == 429 :
319
- time .sleep (int (response .headers ["Retry-After" ]))
320
- else :
321
- break
399
+ log .debug ("HTTP: %s %s -> %s %s" , method , path , response .status_code , response .reason )
400
+ log .debug ("HTTP: Response text -> %s" , response .text )
322
401
323
402
if self .advanced_mode or advanced_mode :
324
403
return response
0 commit comments