11# coding=utf-8
22import logging
3+ import random
34from json import dumps
45
56import requests
910 from oauthlib .oauth1 .rfc5849 import SIGNATURE_RSA_SHA512 as SIGNATURE_RSA
1011except ImportError :
1112 from oauthlib .oauth1 import SIGNATURE_RSA
13+ import time
14+
15+ import urllib3
1216from requests import HTTPError
1317from requests_oauthlib import OAuth1 , OAuth2
1418from six .moves .urllib .parse import urlencode
15- import time
1619from urllib3 .util import Retry
17- import urllib3
1820
1921from atlassian .request_utils import get_default_logger
2022
@@ -69,6 +71,9 @@ def __init__(
6971 retry_status_codes = [413 , 429 , 503 ],
7072 max_backoff_seconds = 1800 ,
7173 max_backoff_retries = 1000 ,
74+ backoff_factor = 1.0 ,
75+ backoff_jitter = 1.0 ,
76+ retry_with_header = True ,
7277 ):
7378 """
7479 init function for the AtlassianRestAPI object.
@@ -102,6 +107,19 @@ def __init__(
102107 wait any longer than this. Defaults to 1800.
103108 :param max_backoff_retries: Maximum number of retries to try before
104109 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.
105123 """
106124 self .url = url
107125 self .username = username
@@ -115,6 +133,14 @@ def __init__(
115133 self .cloud = cloud
116134 self .proxies = proxies
117135 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
118144 if session is None :
119145 self ._session = requests .Session ()
120146 else :
@@ -123,17 +149,17 @@ def __init__(
123149 if proxies is not None :
124150 self ._session .proxies = self .proxies
125151
126- if backoff_and_retry and int ( urllib3 . __version__ . split ( "." )[ 0 ]) >= 2 :
152+ if self . backoff_and_retry and self . use_urllib3_retry :
127153 # Note: we only retry on status and not on any of the
128154 # other supported reasons
129155 retries = Retry (
130156 total = None ,
131- status = max_backoff_retries ,
157+ status = self . max_backoff_retries ,
132158 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 ,
137163 )
138164 self ._session .mount (self .url , HTTPAdapter (max_retries = retries ))
139165 if username and password :
@@ -209,6 +235,59 @@ def _response_handler(response):
209235 log .error (e )
210236 return None
211237
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+
212291 def log_curl_debug (self , method , url , data = None , headers = None , level = logging .DEBUG ):
213292 """
214293
@@ -274,30 +353,32 @@ def request(
274353 :param advanced_mode: bool, OPTIONAL: Return the raw response
275354 :return:
276355 """
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
277373
374+ retry_handler = self ._retry_handler ()
278375 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 )
294376 self .log_curl_debug (
295377 method = method ,
296378 url = url ,
297379 headers = headers ,
298- data = data if data else json_dump ,
380+ data = data or json_dump ,
299381 )
300- headers = headers or self .default_headers
301382 response = self ._session .request (
302383 method = method ,
303384 url = url ,
@@ -310,15 +391,15 @@ def request(
310391 proxies = self .proxies ,
311392 cert = self .cert ,
312393 )
313- response .encoding = "utf-8"
394+ continue_retries = retry_handler (response )
395+ if continue_retries :
396+ continue
397+ break
314398
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"
317400
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 )
322403
323404 if self .advanced_mode or advanced_mode :
324405 return response
0 commit comments