From 7a3724f3fca93b4f55aed5132cf46d3718c4f594 Mon Sep 17 00:00:00 2001 From: Srikanth Chelluri Date: Tue, 8 Jan 2019 20:58:26 -0500 Subject: [PATCH 1/2] fix: handle empty 'Retry-After' header from GitLab When requests are throttled (HTTP response code 429), python-gitlab assumed that 'Retry-After' existed in the response headers. This is not always the case and so the request fails due to a KeyError. The change in this commit adds a rudimentary exponential backoff to the 'http_request' method, which defaults to 10 retries but can be set to -1 to retry without bound. --- docs/api-usage.rst | 16 +++++++++++++++- gitlab/__init__.py | 14 +++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/docs/api-usage.rst b/docs/api-usage.rst index 73d137732..a5afbdaea 100644 --- a/docs/api-usage.rst +++ b/docs/api-usage.rst @@ -299,7 +299,9 @@ Rate limits python-gitlab obeys the rate limit of the GitLab server by default. On receiving a 429 response (Too Many Requests), python-gitlab sleeps for the -amount of time in the Retry-After header that GitLab sends back. +amount of time in the Retry-After header that GitLab sends back. If GitLab +does not return a response with the Retry-After header, python-gitlab will +perform an exponential backoff. If you don't want to wait, you can disable the rate-limiting feature, by supplying the ``obey_rate_limit`` argument. @@ -312,6 +314,18 @@ supplying the ``obey_rate_limit`` argument. gl = gitlab.gitlab(url, token, api_version=4) gl.projects.list(all=True, obey_rate_limit=False) +If you do not disable the rate-limiting feature, you can supply a custom value +for ``max_retries``; by default, this is set to 10. To retry without bound when +throttled, you can set this parameter to -1. This parameter is ignored if +``obey_rate_limit`` is set to ``False``. + +.. code-block:: python + + import gitlab + import requests + + gl = gitlab.gitlab(url, token, api_version=4) + gl.projects.list(all=True, max_retries=12) .. warning:: diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 01f9426d7..c280974e9 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -477,6 +477,10 @@ def http_request(self, verb, path, query_data={}, post_data=None, # obey the rate limit by default obey_rate_limit = kwargs.get("obey_rate_limit", True) + # set max_retries to 10 by default, disable by setting it to -1 + max_retries = kwargs.get("max_retries", 10) + cur_retries = 0 + while True: result = self.session.send(prepped, timeout=timeout, **settings) @@ -486,9 +490,13 @@ def http_request(self, verb, path, query_data={}, post_data=None, return result if 429 == result.status_code and obey_rate_limit: - wait_time = int(result.headers["Retry-After"]) - time.sleep(wait_time) - continue + if max_retries == -1 or cur_retries < max_retries: + wait_time = 2 ** cur_retries * 0.1 + if "Retry-After" in result.headers: + wait_time = int(result.headers["Retry-After"]) + cur_retries += 1 + time.sleep(wait_time) + continue error_message = result.content try: From 16bda20514e036e51bef210b565671174cdeb637 Mon Sep 17 00:00:00 2001 From: Srikanth Chelluri Date: Tue, 8 Jan 2019 22:12:25 -0500 Subject: [PATCH 2/2] fix: remove decode() on error_message string The integration tests failed because a test called 'decode()' on a string-type variable - the GitLabException class handles byte-to-string conversion already in its __init__. This commit removes the call to 'decode()' in the test. ``` Traceback (most recent call last): File "./tools/python_test_v4.py", line 801, in assert 'Retry later' in error_message.decode() AttributeError: 'str' object has no attribute 'decode' ``` --- tools/python_test_v4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py index 30e4456dc..aacb3e7ca 100644 --- a/tools/python_test_v4.py +++ b/tools/python_test_v4.py @@ -798,7 +798,7 @@ except gitlab.GitlabCreateError as e: error_message = e.error_message break -assert 'Retry later' in error_message.decode() +assert 'Retry later' in error_message [current_project.delete() for current_project in projects] settings.throttle_authenticated_api_enabled = False settings.save()