diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index 7052ee1a..2aa44e15 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -65,7 +65,7 @@ line-length = 120 [tool.ruff] target-version = "py311" line-length = 120 -allowed-confusables = ["’"] +allowed-confusables = ["’", "×"] select = [ "A", "ARG", @@ -96,6 +96,7 @@ select = [ ignore = [ "S101", # assert should be allowed "S603", # subprocess with shell=False should be allowed + "S311", # we don’t need cryptographically secure RNG ] unfixable = ["RUF001"] # never “fix” “confusables” diff --git a/scripts/src/scverse_template_scripts/backoff.py b/scripts/src/scverse_template_scripts/backoff.py new file mode 100644 index 00000000..4bbb697b --- /dev/null +++ b/scripts/src/scverse_template_scripts/backoff.py @@ -0,0 +1,23 @@ +import random +import time +from collections.abc import Callable +from typing import TypeVar + +T = TypeVar("T") + + +def retry_with_backoff( + fn: Callable[[], T], + retries: int = 5, + backoff_in_seconds: int | float = 1, + exc_cls: type = Exception, +) -> T: + exc = None + for x in range(retries): + try: + return fn() + except exc_cls as _exc: + exc = _exc + sleep = backoff_in_seconds * 2**x + random.uniform(0, 1) + time.sleep(sleep) + raise exc diff --git a/scripts/src/scverse_template_scripts/cruft_prs.py b/scripts/src/scverse_template_scripts/cruft_prs.py index 901f7b4b..52d5c502 100644 --- a/scripts/src/scverse_template_scripts/cruft_prs.py +++ b/scripts/src/scverse_template_scripts/cruft_prs.py @@ -3,6 +3,7 @@ Uses `template-repos.yml` from `scverse/ecosystem-packages`. """ +import math import os import sys from collections.abc import Generator @@ -17,13 +18,15 @@ from furl import furl from git.repo import Repo from git.util import Actor -from github import ContentFile, Github +from github import ContentFile, Github, UnknownObjectException from github.GitRelease import GitRelease as GHRelease from github.NamedUser import NamedUser from github.PullRequest import PullRequest from github.Repository import Repository as GHRepo from yaml import safe_load +from .backoff import retry_with_backoff + log = getLogger(__name__) PR_BODY_TEMPLATE = """\ @@ -163,10 +166,17 @@ def cruft_update(con: GitHubConnection, repo: GHRepo, path: Path, pr: PR) -> boo return True +# GitHub says that up to 5 minutes of wait are OK, +# So we error our once we wait longer, i.e. when 2ⁿ = 5 min × 60 sec/min +n_retries = math.ceil(math.log(5 * 60) / math.log(2)) # = ⌈~8.22⌉ = 9 +# Due to exponential backoff, we’ll maximally wait 2⁹ sec, or 8.5 min + + def get_fork(con: GitHubConnection, repo: GHRepo) -> GHRepo: if fork := next((f for f in repo.get_forks() if f.owner.id == con.user.id), None): return fork - return repo.create_fork() + fork = repo.create_fork() + return retry_with_backoff(lambda: con.gh.get_repo(fork.id), retries=n_retries, exc_cls=UnknownObjectException) def make_pr(con: GitHubConnection, release: GHRelease, repo_url: str) -> None: