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,59 @@ 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 delay based on the retry count and
243
+ a configurable backoff factor. It optionally adds a random jitter to introduce
244
+ variability in the delay, which can help prevent synchronized retries in
245
+ distributed systems. The calculated backoff delay is clamped between 0 and a
246
+ maximum allowable delay (`self.max_backoff_seconds`) to avoid excessively long
247
+ wait times.
248
+
249
+ :param retry_count: int, REQUIRED: The current retry attempt number (1-based).
250
+ Determines the exponential backoff delay.
251
+ :return: float: The calculated backoff delay in seconds, adjusted for jitter
252
+ and clamped to the maximum allowable value.
253
+ """
254
+ backoff_value = self .backoff_factor * (2 ** (retry_count - 1 ))
255
+ if self .backoff_jitter != 0.0 :
256
+ backoff_value += random .random () * self .backoff_jitter
257
+ return float (max (0 , min (self .max_backoff_seconds , backoff_value )))
258
+
259
+ def _retry_handler (self ):
260
+ """
261
+ Creates and returns a retry handler function for managing HTTP request retries.
262
+
263
+ The returned handler function determines whether a request should be retried
264
+ based on the response and retry settings.
265
+
266
+ :return: Callable[[Response], bool]: A function that takes an HTTP response object as input and
267
+ returns `True` if the request should be retried, or `False` otherwise.
268
+ """
269
+ retries = 0
270
+
271
+ def _handle (response ):
272
+ nonlocal retries
273
+
274
+ if self .retry_with_header and "Retry-After" in response .headers and response .status_code == 429 :
275
+ time .sleep (int (response .headers ["Retry-After" ]))
276
+ return True
277
+
278
+ if not self .backoff_and_retry or self .use_urllib3_retry :
279
+ return False
280
+
281
+ if retries < self .max_backoff_retries and response .status_code in self .retry_status_codes :
282
+ retries += 1
283
+ backoff_value = self ._calculate_backoff_value (retries )
284
+ time .sleep (backoff_value )
285
+ return True
286
+
287
+ return False
288
+
289
+ return _handle
290
+
212
291
def log_curl_debug (self , method , url , data = None , headers = None , level = logging .DEBUG ):
213
292
"""
214
293
@@ -274,30 +353,32 @@ def request(
274
353
:param advanced_mode: bool, OPTIONAL: Return the raw response
275
354
:return:
276
355
"""
356
+ url = self .url_joiner (None if absolute else self .url , path , trailing )
357
+ params_already_in_url = True if "?" in url else False
358
+ if params or flags :
359
+ if params_already_in_url :
360
+ url += "&"
361
+ else :
362
+ url += "?"
363
+ if params :
364
+ url += urlencode (params or {})
365
+ if flags :
366
+ url += ("&" if params or params_already_in_url else "" ) + "&" .join (flags or [])
367
+ json_dump = None
368
+ if files is None :
369
+ data = None if not data else dumps (data )
370
+ json_dump = None if not json else dumps (json )
371
+
372
+ headers = headers or self .default_headers
277
373
374
+ retry_handler = self ._retry_handler ()
278
375
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
376
self .log_curl_debug (
295
377
method = method ,
296
378
url = url ,
297
379
headers = headers ,
298
- data = data if data else json_dump ,
380
+ data = data or json_dump ,
299
381
)
300
- headers = headers or self .default_headers
301
382
response = self ._session .request (
302
383
method = method ,
303
384
url = url ,
@@ -310,15 +391,15 @@ def request(
310
391
proxies = self .proxies ,
311
392
cert = self .cert ,
312
393
)
313
- response .encoding = "utf-8"
394
+ continue_retries = retry_handler (response )
395
+ if continue_retries :
396
+ continue
397
+ break
314
398
315
- log .debug ("HTTP: %s %s -> %s %s" , method , path , response .status_code , response .reason )
316
- log .debug ("HTTP: Response text -> %s" , response .text )
399
+ response .encoding = "utf-8"
317
400
318
- if response .status_code == 429 :
319
- time .sleep (int (response .headers ["Retry-After" ]))
320
- else :
321
- break
401
+ log .debug ("HTTP: %s %s -> %s %s" , method , path , response .status_code , response .reason )
402
+ log .debug ("HTTP: Response text -> %s" , response .text )
322
403
323
404
if self .advanced_mode or advanced_mode :
324
405
return response
0 commit comments