Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upGitHub is where the world builds software
Millions of developers and companies build, ship, and maintain their software on GitHub — the largest and most advanced development platform in the world.
Add automatic rate-limit handling #772
Conversation
Access to GitHub API v3 is rate limited. This change adds checks to all requests to automatically detect if the rate limit is reached. In such a case, the request is stalled until rate limit is refreshed. Rate limit data is read from headers of last response if available. If there is not response available yet, rate limit information is requested from the /rate_limit endpoint of GitHub API v3. For more information see https://developer.github.com/v3/rate_limit/ In addition to the rate limit, GitHub API v3 returns errors when certain abusive behaviour is detected. This change catches such abuse rate limit notifications and waits for the time suggest in Retry-After headers. (cf. https://developer.github.com/v3/#abuse-rate-limits)
What happens if a client is using this library in a threaded way and multiple threads are accessing the API? From reading the code it looks like a wait forever condition, should there be a hard timeout? Can there be an opt-out for consumers that handle rate limiting themselves? |
I have specifically refused to add this automagic handling to github3.py. I'd be happy to have an object that provides an interface to handle rate-limiting but users would always need to opt into that. |
@omgjlk In general I have to admit to not have put any thought into concurrency issues yet. In any case, we need to differentiate between rate limit and abuse detection since they need to be handled quite differently: rate limiting is transparent and can be easily complied with by reading headers. On the other hand, how the abuse detection mechanism works is rather opaque and can only be avoided by common sense and caution or needs to be dealt with after the fact. With that in mind I can say that the logic for dealing with rate limits should work with threads requesting concurrently in principle (bar access to the cached rate limit values). As you point out correctly, the abuse detection handling currently blocks every time and would even increase back off time indefinitely. This is definitely a bad design. Instead of (or complementary to) an upper limit for the wait time, I think it would be best to avoid running into the abuse limit with multiple threads by holding a lock to the entire request method; ie other threads should be blocked from contacting the API in order to avoid worsening the situation. |
@sigmavirus24 I developed this for a project where it is implemented by inheriting from Is this (or another approach) something you would accept? |
@omgjlk, do these suggestions address your concerns appropriately? |
SEARCH_RESOURCE, or GRAPHQL_RESOURCE. | ||
""" | ||
ratelimit = self._get_ratelimit(resource) | ||
if int(ratelimit.get('remaining', '0')) < 1: |
justfortherec
Jan 17, 2018
Author
Contributor
Here ratelimit['remaining']
should be decremented atomically to facilitate parallism
return response is not None and response.status_code == 403 | ||
|
||
def _handle_abuse_detection(self, response): | ||
"""Wait for period suggested in response headers. |
justfortherec
Jan 17, 2018
Author
Contributor
This method should set a flag to indicate that no other request should be sent to API until Retry-after seconds.
self.suggested_time_between_requests *= 2 | ||
time.sleep(retry_after + self.DEFAULT_SLEEP_PERIOD) | ||
|
||
def request(self, method, url, *args, **kwargs): |
justfortherec
Jan 17, 2018
Author
Contributor
This method should block before submitting any request if an abuse detection flag is set. The request having triggered that abuse detection is the only one to clear the flag.
https://developer.github.com/v3/rate_limit/ | ||
""" | ||
if not (self._ratelimit_cache and resource in self._ratelimit_cache): | ||
self._fill_ratelimit_cache() |
justfortherec
Jan 17, 2018
Author
Contributor
_fill_ratelimit_cache
should only be called once. Subsequent calls should wait for the first call to finish.
(Is that really necessary? Do these additional calls to /rate_limit
hurt?)
If you want to. I'm not your boss. I'd love to not have to write this myself but I won't dictate how you spend your free time. I sincerely appreciate the fact that you even wrote and submitted this in the first place.
So I had two approaches in mind and I really didn't have a strong feeling on which way to go with them. The first approach was what you described: A The second approach was a variation on the first: A
This would also allow someone to subclass just the transport adapter and perhaps add sleeps there if they so choose. It would be less work than subclassing the session etc. Finally, I don't know that we want to introduce a new keyword for ratelimiting, but instead provide methods to do the work for them.
|
Nice to hear that you are interested in the functionality. I will try to get it into shape.
Although creating an adapter sounds like a good approach, I prefer extending the session class, because that is what I know already. I might reconsider if the approach I already have is easily adaptable to an adapter.
IMHO, if the library does not add sleeps, then the entire class is superfluous:
What added value would this pull request then bring and why would it need to be opt-in?
Fair enough. |
Some added value could be to create more descriptive Exception classes like This is even backwards compatible because any existing code that excepts I still think that would not need to be opt-in and it is but a tiny fraction of what this pull request was about: Automatic handling of rate limits. |
So if github3.py uses sleeps directly then that removes a the user's ability to handle the ratelimiting the way they want to. This is one area where I don't think we should be prescriptive. I can imagine a large system talking to GitHub hitting its ratelimit and all of a sudden all of the workers are tied up in |
Locally I have replaced most of the changes in this merge request with improved exceptions, as discussed above. The way I had the rate limit handling implemented hooked into sessions, i.e. before exceptions are raised. That means the rate limit handling in this state cannot take advantage of the improved exceptions. You, @sigmavirus24, seem prefer having this separated in such a way that users of the framework need to explicitly opt-in to automatic handling of ratelimit errors. A possibly way of combining this could be by having decorators that a user attaches to any code they want rate limits handled in. The decorators could catch exceptions and introduce sleeps where necessary. This is a mock-up: def sleep_ratelimit_handler_decorator(func):
def wrapper(*args, **kwargs):
while true:
try:
return func(*args, **kwargs)
except RateLimitError as e:
sleep_time = e.ratelimit_reset - datetime.now()
time.sleep(sleep_time.total_seconds)
except AbuseLimitError as e:
time.sleep(e.retry_after)
return wrapper |
I think something like that would be a great addition. How does creating a module, e.g., |
Access to GitHub API v3 is rate limited. This change adds checks to all
requests to automatically detect if the rate limit is reached. In such
a case, the request is stalled until rate limit is refreshed.
Rate limit data is read from headers of last response if available. If
there is not response available yet, rate limit information is requested
from the
/rate_limit
endpoint of GitHub API v3. For more informationsee https://developer.github.com/v3/rate_limit/
In addition to the rate limit, GitHub API v3 returns errors when certain
abusive behaviour is detected. This change catches such abuse rate limit
notifications and waits for the time suggested in
Retry-After
headers.(cf. https://developer.github.com/v3/#abuse-rate-limits)
This pull request does not include any new tests or documentation yet.
The additional requests introduced by this change (e.g. to inquire what the rate limit is in case no response headers are available yet), breaks all kinds of other existing tests.
I would highly appreciate assistance/guidance in fixing the currently failing tests.
Also, documentation needs to be amended to reflect the newly introduced behaviour.
Feel free to suggest any other ways of improving this pull request, so it can be merged.
TODO: