Skip to content

Commit 7519d61

Browse files
Stefano Borierostefanoboriero
Stefano Boriero
authored andcommitted
Handle rate limit error
* add status code 429 to the list of retriable errors FIXES pycontribs#925
1 parent e00c1f7 commit 7519d61

File tree

2 files changed

+48
-6
lines changed

2 files changed

+48
-6
lines changed

jira/resilientsession.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def raise_on_error(r: Optional[Response], verb="???", **kwargs):
8888
class ResilientSession(Session):
8989
"""This class is supposed to retry requests that do return temporary errors.
9090
91-
At this moment it supports: 502, 503, 504
91+
At this moment it supports: 429
9292
"""
9393

9494
def __init__(self, timeout=None):
@@ -113,11 +113,14 @@ def __recoverable(
113113
f"Got ConnectionError [{response}] errno:{response.errno} on {request} {url}\n{vars(response)}\n{response.__dict__}"
114114
)
115115
if isinstance(response, Response):
116-
if response.status_code in [502, 503, 504, 401]:
117-
# 401 UNAUTHORIZED still randomly returned by Atlassian Cloud as of 2017-01-16
116+
if response.status_code in [429]:
117+
rate_limit_remaining = response.headers["X-RateLimit-Remaining"]
118118
msg = f"{response.status_code} {response.reason}"
119-
# 2019-07-25: Disabled recovery for codes above^
120-
return False
119+
logging.warning(
120+
f"""Request rate limited by Jira: number of tokens remaining {rate_limit_remaining}. Consider adding exemption for the user as explained in
121+
https://confluence.atlassian.com/adminjiraserver/improving-instance-stability-with-rate-limiting-983794911.html
122+
"""
123+
)
121124
elif not (
122125
response.status_code == 200
123126
and len(response.content) == 0
@@ -128,7 +131,7 @@ def __recoverable(
128131
else:
129132
msg = "Atlassian's bug https://jira.atlassian.com/browse/JRA-41559"
130133

131-
# Exponential backoff with full jitter.
134+
# Exponential backoff with full jijitter.
132135
delay = min(self.max_retry_delay, 10 * 2**counter) * random.random()
133136
logging.warning(
134137
"Got recoverable error from %s %s, will retry [%s/%s] in %ss. Err: %s"
@@ -137,6 +140,7 @@ def __recoverable(
137140
if isinstance(response, Response):
138141
logging.debug("response.headers: %s", response.headers)
139142
logging.debug("response.body: %s", response.content)
143+
logging.debug(msg)
140144
time.sleep(delay)
141145
return True
142146

tests/test_resilientsession.py

+38
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import logging
2+
from unittest.mock import Mock, patch
3+
4+
import pytest
5+
from requests import Response
26

37
import jira.resilientsession
8+
from jira.exceptions import JIRAError
49
from tests.conftest import JiraTestCase
510

611

@@ -53,3 +58,36 @@ def test_logging_with_connection_error(self):
5358
def tearDown(self):
5459
jira.resilientsession.logging.getLogger().removeHandler(self.loggingHandler)
5560
del self.loggingHandler
61+
62+
63+
@patch("requests.Session.get")
64+
@patch("time.sleep")
65+
def test_throttling_error_is_retried(
66+
mocked_sleep_method: Mock, mocked_get_method: Mock
67+
):
68+
mocked_throttled_response: Response = Response()
69+
mocked_throttled_response.status_code = 429
70+
mocked_throttled_response.headers["X-RateLimit-Remaining"] = "1"
71+
mocked_get_method.return_value = mocked_throttled_response
72+
session: jira.resilientsession.ResilientSession = (
73+
jira.resilientsession.ResilientSession()
74+
)
75+
with pytest.raises(JIRAError):
76+
session.get("mocked_url")
77+
assert mocked_get_method.call_count == 4
78+
assert mocked_sleep_method.call_count == 3
79+
80+
81+
@patch("requests.Session.get")
82+
@patch("time.sleep")
83+
def test_5xx_error_is_not_retried(mocked_sleep_method: Mock, mocked_get_method: Mock):
84+
mocked_5xx_response: Response = Response()
85+
mocked_5xx_response.status_code = 502
86+
mocked_get_method.return_value = mocked_5xx_response
87+
session: jira.resilientsession.ResilientSession = (
88+
jira.resilientsession.ResilientSession()
89+
)
90+
with pytest.raises(JIRAError):
91+
session.get("mocked_url")
92+
assert mocked_get_method.call_count == 1
93+
assert mocked_sleep_method.call_count == 0

0 commit comments

Comments
 (0)