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,25 @@ 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
+
212
257
def log_curl_debug(self, method, url, data=None, headers=None, level=logging.DEBUG):
213
258
"""
214
259
@@ -274,30 +319,32 @@ def request(
274
319
:param advanced_mode: bool, OPTIONAL: Return the raw response
275
320
:return:
276
321
"""
322
+ url = self.url_joiner(None if absolute else self.url, path, trailing)
323
+ params_already_in_url = True if "?" in url else False
324
+ if params or flags:
325
+ if params_already_in_url:
326
+ url += "&"
327
+ else:
328
+ url += "?"
329
+ if params:
330
+ url += urlencode(params or {})
331
+ if flags:
332
+ url += ("&" if params or params_already_in_url else "") + "&".join(flags or [])
333
+ json_dump = None
334
+ if files is None:
335
+ data = None if not data else dumps(data)
336
+ json_dump = None if not json else dumps(json)
277
337
338
+ headers = headers or self.default_headers
339
+
340
+ retries = 0
278
341
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
342
self.log_curl_debug(
295
343
method=method,
296
344
url=url,
297
345
headers=headers,
298
- data=data if data else json_dump,
346
+ data=data or json_dump,
299
347
)
300
- headers = headers or self.default_headers
301
348
response = self._session.request(
302
349
method=method,
303
350
url=url,
@@ -310,16 +357,27 @@ def request(
310
357
proxies=self.proxies,
311
358
cert=self.cert,
312
359
)
313
- response.encoding = "utf-8"
314
360
315
- log.debug("HTTP: %s %s -> %s %s", method, path, response.status_code, response.reason)
316
- log.debug("HTTP: Response text -> %s", response.text)
317
-
318
- if response.status_code == 429:
361
+ if self.retry_with_header and "Retry-After" in response.headers and response.status_code == 429:
319
362
time.sleep(int(response.headers["Retry-After"]))
320
- else:
363
+ continue
364
+
365
+ if not self.backoff_and_retry or self.use_urllib3_retry:
321
366
break
322
367
368
+ if retries < self.max_backoff_retries and response.status_code in self.retry_status_codes:
369
+ retries += 1
370
+ backoff_value = self._calculate_backoff_value(retries)
371
+ time.sleep(backoff_value)
372
+ continue
373
+
374
+ break
375
+
376
+ response.encoding = "utf-8"
377
+
378
+ log.debug("HTTP: %s %s -> %s %s", method, path, response.status_code, response.reason)
379
+ log.debug("HTTP: Response text -> %s", response.text)
380
+
323
381
if self.advanced_mode or advanced_mode:
324
382
return response
325
383
0 commit comments