Skip to content

Commit 3e1c625

Browse files
committed
chore: add test, docs, and helper for 409 retries
1 parent dced76a commit 3e1c625

File tree

3 files changed

+79
-10
lines changed

3 files changed

+79
-10
lines changed

docs/api-usage-advanced.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,14 @@ GitLab server can sometimes return a transient HTTP error.
123123
python-gitlab can automatically retry in such case, when
124124
``retry_transient_errors`` argument is set to ``True``. When enabled,
125125
HTTP error codes 500 (Internal Server Error), 502 (502 Bad Gateway),
126-
503 (Service Unavailable), and 504 (Gateway Timeout) are retried.
127-
Additionally the HTTP error code 409 (Conflict) is retried if the text message
128-
mentions "Resource lock". It will retry until reaching the ``max_retries``
126+
503 (Service Unavailable), 504 (Gateway Timeout), and Cloudflare
127+
errors (520-530) are retried.
128+
129+
Additionally, HTTP error code 409 (Conflict) is retried if the reason
130+
is a
131+
`Resource lock <https://gitlab.com/gitlab-org/gitlab/-/blob/443c12cf3b238385db728f03b2cdbb4f17c70292/lib/api/api.rb#L111>`__.
132+
133+
It will retry until reaching the ``max_retries``
129134
value. By default, ``retry_transient_errors`` is set to ``False`` and an
130135
exception is raised for these errors.
131136

gitlab/client.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -751,13 +751,20 @@ def http_request(
751751
if 200 <= result.status_code < 300:
752752
return result.response
753753

754-
if (429 == result.status_code and obey_rate_limit) or (
755-
(
756-
result.status_code in gitlab.const.RETRYABLE_TRANSIENT_ERROR_CODES
757-
or (result.status_code == 409 and "Resource lock" in result.reason)
758-
)
759-
and retry_transient_errors
760-
):
754+
def should_retry() -> bool:
755+
if result.status_code == 429 and obey_rate_limit:
756+
return True
757+
758+
if not retry_transient_errors:
759+
return False
760+
if result.status_code in gitlab.const.RETRYABLE_TRANSIENT_ERROR_CODES:
761+
return True
762+
if result.status_code == 409 and "Resource lock" in result.reason:
763+
return True
764+
765+
return False
766+
767+
if should_retry():
761768
# Response headers documentation:
762769
# https://docs.gitlab.com/ee/user/admin_area/settings/user_and_ip_rate_limits.html#response-headers
763770
if max_retries == -1 or cur_retries < max_retries:

tests/unit/test_gitlab_http_methods.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,63 @@ def response_callback(
372372
assert "http://example.com/api/v4/user/status" in error_message
373373

374374

375+
def test_http_request_on_409_resource_lock_retries(gl_retry):
376+
url = "http://localhost/api/v4/user"
377+
retried = False
378+
379+
def response_callback(
380+
response: requests.models.Response,
381+
) -> requests.models.Response:
382+
"""We need a callback that adds a resource lock reason only on first call"""
383+
nonlocal retried
384+
385+
if not retried:
386+
response.reason = "Resource lock"
387+
388+
retried = True
389+
return response
390+
391+
with responses.RequestsMock(response_callback=response_callback) as rsps:
392+
rsps.add(
393+
method=responses.GET,
394+
url=url,
395+
status=409,
396+
match=helpers.MATCH_EMPTY_QUERY_PARAMS,
397+
)
398+
rsps.add(
399+
method=responses.GET,
400+
url=url,
401+
status=200,
402+
match=helpers.MATCH_EMPTY_QUERY_PARAMS,
403+
)
404+
response = gl_retry.http_request("get", "/user")
405+
406+
assert response.status_code == 200
407+
408+
409+
def test_http_request_on_409_resource_lock_without_retry_raises(gl):
410+
url = "http://localhost/api/v4/user"
411+
412+
def response_callback(
413+
response: requests.models.Response,
414+
) -> requests.models.Response:
415+
"""Without retry, this will fail on the first call"""
416+
response.reason = "Resource lock"
417+
return response
418+
419+
with responses.RequestsMock(response_callback=response_callback) as req_mock:
420+
req_mock.add(
421+
method=responses.GET,
422+
url=url,
423+
status=409,
424+
match=helpers.MATCH_EMPTY_QUERY_PARAMS,
425+
)
426+
with pytest.raises(GitlabHttpError) as excinfo:
427+
gl.http_request("get", "/user")
428+
429+
assert excinfo.value.response_code == 409
430+
431+
375432
@responses.activate
376433
def test_get_request(gl):
377434
url = "http://localhost/api/v4/projects"

0 commit comments

Comments
 (0)