Skip to content

Commit 78ec901

Browse files
ezyangbdaz
andauthored
Respect GitHub ratelimit to support more than 8 stacked PRs (#290)
Co-authored-by: Brian Amerige <[email protected]>
1 parent 9051cec commit 78ec901

File tree

3 files changed

+67
-40
lines changed

3 files changed

+67
-40
lines changed

src/ghstack/github_real.py

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
import json
44
import logging
55
import re
6+
import time
67
from typing import Any, Dict, Optional, Sequence, Tuple, Union
78

89
import requests
910

1011
import ghstack.github
1112

13+
MAX_RETRIES = 5
14+
INITIAL_BACKOFF_SECONDS = 60
15+
1216

1317
class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
1418
"""
@@ -155,32 +159,63 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any:
155159
}
156160

157161
url = self.rest_endpoint.format(github_url=self.github_url) + "/" + path
158-
logging.debug("# {} {}".format(method, url))
159-
logging.debug("Request body:\n{}".format(json.dumps(kwargs, indent=1)))
160162

161-
resp: requests.Response = getattr(requests, method)(
162-
url,
163-
json=kwargs,
164-
headers=headers,
165-
proxies=self._proxies(),
166-
verify=self.verify,
167-
cert=self.cert,
168-
)
163+
backoff_seconds = INITIAL_BACKOFF_SECONDS
164+
for attempt in range(0, MAX_RETRIES):
165+
logging.debug("# {} {}".format(method, url))
166+
logging.debug("Request body:\n{}".format(json.dumps(kwargs, indent=1)))
169167

170-
logging.debug("Response status: {}".format(resp.status_code))
168+
resp: requests.Response = getattr(requests, method)(
169+
url,
170+
json=kwargs,
171+
headers=headers,
172+
proxies=self._proxies(),
173+
verify=self.verify,
174+
cert=self.cert,
175+
)
171176

172-
try:
173-
r = resp.json()
174-
except ValueError:
175-
logging.debug("Response body:\n{}".format(r.text))
176-
raise
177-
else:
178-
pretty_json = json.dumps(r, indent=1)
179-
logging.debug("Response JSON:\n{}".format(pretty_json))
177+
logging.debug("Response status: {}".format(resp.status_code))
180178

181-
if resp.status_code == 404:
182-
raise ghstack.github.NotFoundError(
183-
"""\
179+
try:
180+
r = resp.json()
181+
except ValueError:
182+
logging.debug("Response body:\n{}".format(r.text))
183+
raise
184+
else:
185+
pretty_json = json.dumps(r, indent=1)
186+
logging.debug("Response JSON:\n{}".format(pretty_json))
187+
188+
# Per Github rate limiting: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#exceeding-the-rate-limit
189+
if resp.status_code in (403, 429):
190+
remaining_count = resp.headers.get("x-ratelimit-remaining")
191+
reset_time = resp.headers.get("x-ratelimit-reset")
192+
193+
if remaining_count == "0" and reset_time:
194+
sleep_time = int(reset_time) - int(time.time())
195+
logging.warning(
196+
f"Rate limit exceeded. Sleeping until reset in {sleep_time} seconds."
197+
)
198+
time.sleep(sleep_time)
199+
continue
200+
else:
201+
retry_after_seconds = resp.headers.get("retry-after")
202+
if retry_after_seconds:
203+
sleep_time = int(retry_after_seconds)
204+
logging.warning(
205+
f"Secondary rate limit hit. Sleeping for {sleep_time} seconds."
206+
)
207+
else:
208+
sleep_time = backoff_seconds
209+
logging.warning(
210+
f"Secondary rate limit hit. Sleeping for {sleep_time} seconds (exponential backoff)."
211+
)
212+
backoff_seconds *= 2
213+
time.sleep(sleep_time)
214+
continue
215+
216+
if resp.status_code == 404:
217+
raise ghstack.github.NotFoundError(
218+
"""\
184219
GitHub raised a 404 error on the request for
185220
{url}.
186221
Usually, this doesn't actually mean the page doesn't exist; instead, it
@@ -190,13 +225,15 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any:
190225
"public_repo" for permissions, and update ~/.ghstackrc with your new
191226
value.
192227
""".format(
193-
url=url, github_url=self.github_url
228+
url=url, github_url=self.github_url
229+
)
194230
)
195-
)
196231

197-
try:
198-
resp.raise_for_status()
199-
except requests.HTTPError:
200-
raise RuntimeError(pretty_json)
232+
try:
233+
resp.raise_for_status()
234+
except requests.HTTPError:
235+
raise RuntimeError(pretty_json)
201236

202-
return r
237+
return r
238+
239+
raise RuntimeError("Exceeded maximum retries due to GitHub rate limiting")

src/ghstack/submit.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -468,12 +468,6 @@ def run(self) -> List[DiffMeta]:
468468
raise RuntimeError(
469469
"There appears to be no commits to process, based on the revs you passed me."
470470
)
471-
elif commit_count > 8 and not self.force:
472-
raise RuntimeError(
473-
"Cowardly refusing to handle a stack with more than eight PRs. "
474-
"You are likely to get rate limited by GitHub if you try to create or "
475-
"manipulate this many PRs. You can bypass this throttle using --force"
476-
)
477471

478472
# This is not really accurate if you're doing a fancy pattern;
479473
# if this is a problem file us a bug.

test/submit/throttle.py.test

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ init_test()
55
for i in range(10):
66
commit(f"A{i}")
77

8-
assert_expected_raises_inline(
9-
RuntimeError,
10-
lambda: gh_submit("Initial"),
11-
"""Cowardly refusing to handle a stack with more than eight PRs. You are likely to get rate limited by GitHub if you try to create or manipulate this many PRs. You can bypass this throttle using --force""",
12-
)
8+
gh_submit("Initial")
139

1410
ok()

0 commit comments

Comments
 (0)