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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy