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,57 @@ 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 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+
212289 def log_curl_debug (self , method , url , data = None , headers = None , level = logging .DEBUG ):
213290 """
214291
@@ -274,30 +351,32 @@ def request(
274351 :param advanced_mode: bool, OPTIONAL: Return the raw response
275352 :return:
276353 """
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
277371
372+ retry_handler = self ._retry_handler ()
278373 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 )
294374 self .log_curl_debug (
295375 method = method ,
296376 url = url ,
297377 headers = headers ,
298- data = data if data else json_dump ,
378+ data = data or json_dump ,
299379 )
300- headers = headers or self .default_headers
301380 response = self ._session .request (
302381 method = method ,
303382 url = url ,
@@ -310,15 +389,15 @@ def request(
310389 proxies = self .proxies ,
311390 cert = self .cert ,
312391 )
313- response .encoding = "utf-8"
392+ continue_retries = retry_handler (response )
393+ if continue_retries :
394+ continue
395+ break
314396
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"
317398
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 )
322401
323402 if self .advanced_mode or advanced_mode :
324403 return response
0 commit comments