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,25 @@ 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+
212257 def log_curl_debug (self , method , url , data = None , headers = None , level = logging .DEBUG ):
213258 """
214259
@@ -274,30 +319,32 @@ def request(
274319 :param advanced_mode: bool, OPTIONAL: Return the raw response
275320 :return:
276321 """
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 )
277337
338+ headers = headers or self .default_headers
339+
340+ retries = 0
278341 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 )
294342 self .log_curl_debug (
295343 method = method ,
296344 url = url ,
297345 headers = headers ,
298- data = data if data else json_dump ,
346+ data = data or json_dump ,
299347 )
300- headers = headers or self .default_headers
301348 response = self ._session .request (
302349 method = method ,
303350 url = url ,
@@ -310,16 +357,27 @@ def request(
310357 proxies = self .proxies ,
311358 cert = self .cert ,
312359 )
313- response .encoding = "utf-8"
314360
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 :
319362 time .sleep (int (response .headers ["Retry-After" ]))
320- else :
363+ continue
364+
365+ if not self .backoff_and_retry or self .use_urllib3_retry :
321366 break
322367
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+
323381 if self .advanced_mode or advanced_mode :
324382 return response
325383
0 commit comments