Skip to content

Commit 869483b

Browse files
committed
Add configurable backoff
In certain contexts, removing the backoff makes sense. In continuous integration, we may prefer to keep the number of attempts at most to three, to avoid paying for sleep. Signed-off-by: William Phetsinorath <william.phetsinorath@shikanime.studio> Change-Id: I9b8ceabdf28310432615f7c77c4a08486a6a6964
1 parent 3987ada commit 869483b

3 files changed

Lines changed: 61 additions & 20 deletions

File tree

src/ghstack/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def cli_context(
4444
oauth_token=config.github_oauth,
4545
proxy=config.proxy,
4646
github_url=config.github_url,
47+
max_retries=config.max_retries,
48+
initial_backoff_seconds=config.initial_backoff_seconds,
4749
)
4850
yield shell, config, github
4951

src/ghstack/config.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ def get_gh_cli_credentials(
111111
("reviewer", Optional[str]),
112112
# Default labels to add to new pull requests (comma-separated labels)
113113
("label", Optional[str]),
114+
# Retry/backoff tuning
115+
("max_retries", int),
116+
# The initial backoff time to use, in seconds. We will double this
117+
# time for each retry.
118+
("initial_backoff_seconds", int),
114119
],
115120
)
116121

@@ -301,6 +306,18 @@ def read_config(
301306
else:
302307
label = None
303308

309+
if config.has_option("ghstack", "max_retries"):
310+
max_retries = config.getint("ghstack", "max_retries")
311+
else:
312+
max_retries = 5
313+
314+
if config.has_option("ghstack", "initial_backoff_seconds"):
315+
initial_backoff_seconds = config.getint(
316+
"ghstack", "initial_backoff_seconds"
317+
)
318+
else:
319+
initial_backoff_seconds = 60
320+
304321
if write_back:
305322
with open(config_path, "w") as f:
306323
config.write(f)
@@ -318,6 +335,8 @@ def read_config(
318335
remote_name=remote_name,
319336
reviewer=reviewer,
320337
label=label,
338+
max_retries=max_retries,
339+
initial_backoff_seconds=initial_backoff_seconds,
321340
)
322341
logging.debug(f"conf = {conf}")
323342
return conf

src/ghstack/github_real.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@
1010

1111
import ghstack.github
1212

13-
MAX_RETRIES = 5
14-
INITIAL_BACKOFF_SECONDS = 60
15-
1613

1714
class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
1815
"""
@@ -57,19 +54,30 @@ def rest_endpoint(self) -> str:
5754
# Passed to requests as 'cert'.
5855
cert: Optional[Union[str, Tuple[str, str]]]
5956

57+
# The maximum number of times to retry a request before giving up.
58+
max_retries: int
59+
60+
# The initial backoff time to use, in seconds. We will double this
61+
# time for each retry.
62+
initial_backoff_seconds: int
63+
6064
def __init__(
6165
self,
6266
oauth_token: Optional[str],
6367
github_url: str,
6468
proxy: Optional[str] = None,
6569
verify: Optional[str] = None,
6670
cert: Optional[Union[str, Tuple[str, str]]] = None,
71+
max_retries: int = 5,
72+
initial_backoff_seconds: int = 60,
6773
):
6874
self.oauth_token = oauth_token
6975
self.proxy = proxy
7076
self.github_url = github_url
7177
self.verify = verify
7278
self.cert = cert
79+
self.max_retries = max_retries
80+
self.initial_backoff_seconds = initial_backoff_seconds
7381

7482
def push_hook(self, refName: Sequence[str]) -> None:
7583
pass
@@ -160,8 +168,8 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any:
160168

161169
url = self.rest_endpoint.format(github_url=self.github_url) + "/" + path
162170

163-
backoff_seconds = INITIAL_BACKOFF_SECONDS
164-
for attempt in range(0, MAX_RETRIES):
171+
backoff_seconds = self.initial_backoff_seconds
172+
for attempt in range(0, self.max_retries):
165173
logging.debug("# {} {}".format(method, url))
166174
logging.debug("Request body:\n{}".format(json.dumps(kwargs, indent=1)))
167175

@@ -189,29 +197,41 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any:
189197
if resp.status_code in (403, 429):
190198
remaining_count = resp.headers.get("x-ratelimit-remaining")
191199
reset_time = resp.headers.get("x-ratelimit-reset")
200+
more_attempts = attempt < (self.max_retries - 1)
192201

193202
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
203+
sleep_time = max(0, int(reset_time) - int(time.time()))
204+
if more_attempts and sleep_time > 0:
205+
logging.warning(
206+
f"Rate limit exceeded. Sleeping until reset in {sleep_time} seconds."
207+
)
208+
time.sleep(sleep_time)
209+
continue
210+
else:
211+
raise RuntimeError(pretty_json)
200212
else:
201213
retry_after_seconds = resp.headers.get("retry-after")
202214
if retry_after_seconds:
203215
sleep_time = int(retry_after_seconds)
204-
logging.warning(
205-
f"Secondary rate limit hit. Sleeping for {sleep_time} seconds."
206-
)
216+
if more_attempts and sleep_time > 0:
217+
logging.warning(
218+
f"Secondary rate limit hit. Sleeping for {sleep_time} seconds."
219+
)
220+
time.sleep(sleep_time)
221+
continue
222+
else:
223+
raise RuntimeError(pretty_json)
207224
else:
208225
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
226+
if more_attempts and sleep_time > 0:
227+
logging.warning(
228+
f"Secondary rate limit hit. Sleeping for {sleep_time} seconds (exponential backoff)."
229+
)
230+
backoff_seconds *= 2
231+
time.sleep(sleep_time)
232+
continue
233+
else:
234+
raise RuntimeError(pretty_json)
215235

216236
if resp.status_code == 404:
217237
raise ghstack.github.NotFoundError(

0 commit comments

Comments
 (0)