Skip to content

Fix missing "Retry-After" header and fix integration tests #678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 13, 2019

Conversation

srikanthchelluri
Copy link
Contributor

@srikanthchelluri srikanthchelluri commented Jan 9, 2019

This PR does two things: a) implements an exponential backoff in the case that we are throttled and "Retry-After" isn't in the response header (this currently causes a KeyError), b) fix an integration test because we were decoding something that was already a string.

Closes #632.

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.
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 <module>
    assert 'Retry later' in error_message.decode()
AttributeError: 'str' object has no attribute 'decode'
```
@max-wittig
Copy link
Member

max-wittig commented Jan 9, 2019

Thanks for this contribution! Looks pretty good. Any way to easily test this? (e.g. against a public GitLab instance without Retry-After header?)

@srikanthchelluri
Copy link
Contributor Author

srikanthchelluri commented Jan 10, 2019

@max-wittig Hm, this is a bit difficult to test. But here's how I validated the code path - let me know what you think.

  1. Created a virtual environment and built the library with the following code:
while True:
    # result = self.session.send(prepped, timeout=timeout, **settings)
    result = requests.get('https://httpstat.us/429')

    self._check_redirects(result)

    if 200 <= result.status_code < 300:
        return result

    if 429 == result.status_code and obey_rate_limit:
        if max_retries == -1 or cur_retries < max_retries:
            wait_time = 2 ** cur_retries * 0.1
            if "Retry-AfterXYZ" in result.headers:
                wait_time = int(result.headers["Retry-After"])
            cur_retries += 1
            print('DEBUG: backing off', str(wait_time))
            time.sleep(wait_time)
            continue

Note that https://httpstat.us returns a response with the Retry-After header set to 3600. I first validated that we backed off 3600 seconds; then I changed the header we were checking for (Retry-AfterXYZ as shown above) and validated that we were backing off exponentially (see below).

  1. Wrote this small script:
from getpass import getpass
import gitlab

private_token = getpass('GitLab API Token: ')
gl = gitlab.Gitlab('https://gitlab.com/', private_token=private_token)
project = gl.projects.get(<some-project-id-we-own>)

project.members.list(all=True)
  1. Validated the exponential backoff:
(env) ➜  python-gitlab python throttle.py 
GitLab API Token: 
DEBUG: backing off 0.1
DEBUG: backing off 0.2
DEBUG: backing off 0.4
DEBUG: backing off 0.8
DEBUG: backing off 1.6
DEBUG: backing off 3.2
DEBUG: backing off 6.4
DEBUG: backing off 12.8
DEBUG: backing off 25.6
DEBUG: backing off 51.2
Traceback (most recent call last):
  File "/Users/srikanth.chelluri/repo/python-gitlab/gitlab/exceptions.py", line 251, in wrapped_f
    return f(*args, **kwargs)
  File "/Users/srikanth.chelluri/repo/python-gitlab/gitlab/mixins.py", line 49, in get
    server_data = self.gitlab.http_get(path, **kwargs)
  File "/Users/srikanth.chelluri/repo/python-gitlab/gitlab/__init__.py", line 542, in http_get
    streamed=streamed, **kwargs)
  File "/Users/srikanth.chelluri/repo/python-gitlab/gitlab/__init__.py", line 520, in http_request
    response_body=result.content)
gitlab.exceptions.GitlabHttpError: 429: 429 Too Many Requests

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "throttle.py", line 10, in <module>
    project = gl.projects.get(8244461)
  File "/Users/srikanth.chelluri/repo/python-gitlab/gitlab/exceptions.py", line 253, in wrapped_f
    raise error(e.error_message, e.response_code, e.response_body)
gitlab.exceptions.GitlabGetError: 429: 429 Too Many Requests

Thoughts/feedback on this approach? Didn't try spinning up and configuring a GitLab instance without the header since we don't really need it to test the code path.

@max-wittig max-wittig self-assigned this Jan 12, 2019
@max-wittig
Copy link
Member

@srikanthchelluri Works as expected. Many thanks for the contribution 👍

Just a hint for future contributions: It's best practice to use a separate branch in your fork. Makes it easier for us to checkout the changes and easier for you to maintain your next contribution. Otherwise your master in your fork is polluted by another commit etc.

@max-wittig max-wittig merged commit 89679ce into python-gitlab:master Jan 13, 2019
@srikanthchelluri
Copy link
Contributor Author

@max-wittig This was a separate branch, no? I forked the repo into my org (appian), then checked out a new branch called backoff-requests. Let me know if there's a better practice.

Thanks for the review and merge 👍

@max-wittig
Copy link
Member

max-wittig commented Jan 13, 2019

ah really. Okay weird. Nevermind. Maybe I miss-typed something and didn't see it. Not sure 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Retry-After header may be missing: need default timeout
2 participants