8000 Merge pull request #1904 from Sineaggi/retry-additional-http-transien… · python-gitlab/python-gitlab@0353bd4 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0353bd4

Browse files
authored
Merge pull request #1904 from Sineaggi/retry-additional-http-transient-errors
Retry additional http transient errors
2 parents 19ab07d + 5cbbf26 commit 0353bd4

File tree

2 files changed

+126
-16
lines changed

2 files changed

+126
-16
lines changed

gitlab/client.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
"{source!r} to {target!r}"
3636
)
3737

38+
RETRYABLE_TRANSIENT_ERROR_CODES = [500, 502, 503, 504] + list(range(520, 531))
39+
3840

3941
class Gitlab:
4042
"""Represents a GitLab server connection.
@@ -675,30 +677,42 @@ def http_request(
675677
json, data, content_type = self._prepare_send_data(files, post_data, raw)
676678
opts["headers"]["Content-type"] = content_type
677679

680+
retry_transient_errors = kwargs.get(
681+
"retry_transient_errors", self.retry_transient_errors
682+
)
678683
cur_retries = 0
679684
while True:
680-
result = self.session.request(
681-
method=verb,
682-
url=url,
683-
json=json,
684-
data=data,
685-
params=params,
686-
timeout=timeout,
687-
verify=verify,
688-
stream=streamed,
689-
**opts,
690-
)
685+
try:
686+
result = self.session.request(
687+
method=verb,
688+
url=url,
689+
json=json,
690+
data=data,
691+
params=params,
692+
timeout=timeout,
693+
verify=verify,
694+
stream=streamed,
695+
**opts,
696+
)
697+
except requests.ConnectionError:
698+
if retry_transient_errors and (
699+
max_retries == -1 or cur_retries < max_retries
700+
):
701+
wait_time = 2**cur_retries * 0.1
702+
cur_retries += 1
703+
time.sleep(wait_time)
704+
continue
705+
706+
raise
691707

692708
self._check_redirects(result)
693709

694710
if 200 <= result.status_code < 300:
695711
return result
696712

697-
retry_transient_errors = kwargs.get(
698-
"retry_transient_errors", self.retry_transient_errors
699-
)
700713
if (429 == result.status_code and obey_rate_limit) or (
701-
result.status_code in [500, 502, 503, 504] and retry_transient_errors
714+
result.status_code in RETRYABLE_TRANSIENT_ERROR_CODES
715+
and retry_transient_errors
702716
):
703717
# Response headers documentation:
704718
# https://docs.gitlab.com/ee/user/admin_area/settings/user_and_ip_rate_limits.html#response-headers

tests/unit/test_gitlab_http_methods.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import responses
44

55
from gitlab import GitlabHttpError, GitlabList, GitlabParsingError, RedirectError
6+
from gitlab.client import RETRYABLE_TRANSIENT_ERROR_CODES
67
from tests.unit import helpers
78

89
MATCH_EMPTY_QUERY_PARAMS = [responses.matchers.query_param_matcher({})]
@@ -51,7 +52,7 @@ def test_http_request_404(gl):
5152

5253

5354
@responses.activate
54-
@pytest.mark.parametrize("status_code", [500, 502, 503, 504])
55+
@pytest.mark.parametrize("status_code", RETRYABLE_TRANSIENT_ERROR_CODES)
5556
def test_http_request_with_only_failures(gl, status_code):
5657
url = "http://localhost/api/v4/projects"
5758
responses.add(
@@ -97,6 +98,37 @@ def request_callback(request):
9798
assert len(responses.calls) == calls_before_success
9899

99100

101+
@responses.activate
102+
def test_http_request_with_retry_on_method_for_transient_network_failures(gl):
103+
call_count = 0
104+
calls_before_success = 3
105+
106+
url = "http://localhost/api/v4/projects"
107+
108+
def request_callback(request):
109+
nonlocal call_count
110+
call_count += 1
111+
status_code = 200
112+
headers = {}
113+
body = "[]"
114+
115+
if call_count >= calls_before_success:
116+
return (status_code, headers, body)
117+
raise requests.ConnectionError("Connection aborted.")
118+
119+
responses.add_callback(
120+
method=responses.GET,
121+
url=url,
122+
callback=request_callback,
123+
content_type="application/json",
124+
)
125+
126+
http_r = gl.http_request("get", "/projects", retry_transient_errors=True)
127+
128+
assert http_r.status_code == 200
129+
assert len(responses.calls) == calls_before_success
130+
131+
100132
@responses.activate
101133
def test_http_request_with_retry_on_class_for_transient_failures(gl_retry):
102134
call_count = 0
@@ -126,6 +158,37 @@ def request_callback(request: requests.models.PreparedRequest):
126158
assert len(responses.calls) == calls_before_success
127159

128160

161+
@responses.activate
162+
def test_http_request_with_retry_on_class_for_transient_network_failures(gl_retry):
163+
call_count = 0
164+
calls_before_success = 3
165+
166+
url = "http://localhost/api/v4/projects"
167+
168+
def request_callback(request: requests.models.PreparedRequest):
169+
nonlocal call_count
170+
call_count += 1
171+
status_code = 200
172+
headers = {}
173+
body = "[]"
174+
175+
if call_count >= calls_before_success:
176+
return (status_code, headers, body)
177+
raise requests.ConnectionError("Connection aborted.")
178+
179+
responses.add_callback(
180+
method=responses.GET,
181+
url=url,
182+
callback=request_callback,
183+
content_type="application/json",
184+
)
185+
186+
http_r = gl_retry.http_request("get", "/projects", retry_transient_errors=True)
187+
188+
assert http_r.status_code == 200
189+
assert len(responses.calls) == calls_before_success
190+
191+
129192
@responses.activate
130193
def test_http_request_with_retry_on_class_and_method_for_transient_failures(gl_retry):
131194
call_count = 0
@@ -155,6 +218,39 @@ def request_callback(request):
155218
assert len(responses.calls) == 1
156219

157220

221+
@responses.activate
222+
def test_http_request_with_retry_on_class_and_method_for_transient_network_failures(
223+
gl_retry,
224+
):
225+
call_count = 0
226+
calls_before_success = 3
227+
228+
url = "http://localhost/api/v4/projects"
229+
230+
def request_callback(request):
231+
nonlocal call_count
232+
call_count += 1
233+
status_code = 200
234+
headers = {}
235+
body = "[]"
236+
237+
if call_count >= calls_before_success:
238+
return (status_code, headers, body)
239+
raise requests.ConnectionError("Connection aborted.")
240+
241+
responses.add_callback(
242+
method=responses.GET,
243+
url=url,
244+
callback=request_callback,
245+
content_type="application/json",
246+
)
247+
248+
with pytest.raises(requests.ConnectionError):
249+
gl_retry.http_request("get", "/projects", retry_transient_errors=False)
250+
251+
assert len(responses.calls) == 1
252+
253+
158254
def create_redirect_response(
159255
*, response: requests.models.Response, http_method: str, api_path: str
160256
) -> requests.models.Response:

0 commit comments

Comments
 (0)
0