Skip to content

Commit e804943

Browse files
kwzrdChrisLovering
authored andcommitted
Branding: retry GitHub server errors
Use the tenacity lib to retry 5xx responses from GitHub.
1 parent 79b6d5a commit e804943

File tree

1 file changed

+35
-8
lines changed

1 file changed

+35
-8
lines changed

bot/exts/backend/branding/_repository.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from datetime import UTC, date, datetime
33

44
import frontmatter
5+
from aiohttp import ClientResponse, ClientResponseError
6+
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
57

68
from bot.bot import Bot
79
from bot.constants import Keys
@@ -71,6 +73,35 @@ def __str__(self) -> str:
7173
return f"<Event at '{self.path}'>"
7274

7375

76+
class GitHubServerError(Exception):
77+
"""
78+
GitHub responded with 5xx status code.
79+
80+
Such error shall be retried.
81+
"""
82+
83+
84+
def _raise_for_status(resp: ClientResponse) -> None:
85+
"""Raise custom error if resp status is 5xx."""
86+
# Use the response's raise_for_status so that we can
87+
# attach the full traceback to our custom error.
88+
log.trace(f"GitHub response status: {resp.status}")
89+
try:
90+
resp.raise_for_status()
91+
except ClientResponseError as err:
92+
if resp.status >= 500:
93+
raise GitHubServerError from err
94+
raise
95+
96+
97+
_retry_fetch = retry(
98+
retry=retry_if_exception_type(GitHubServerError), # Only retry this error.
99+
stop=stop_after_attempt(5), # Up to 5 attempts.
100+
wait=wait_exponential(), # Exponential backoff: 1, 2, 4, 8 seconds.
101+
reraise=True, # After final failure, re-raise original exception.
102+
)
103+
104+
74105
class BrandingRepository:
75106
"""
76107
Branding repository abstraction.
@@ -93,6 +124,7 @@ class BrandingRepository:
93124
def __init__(self, bot: Bot) -> None:
94125
self.bot = bot
95126

127+
@_retry_fetch
96128
async def fetch_directory(self, path: str, types: t.Container[str] = ("file", "dir")) -> dict[str, RemoteObject]:
97129
"""
98130
Fetch directory found at `path` in the branding repository.
@@ -105,14 +137,12 @@ async def fetch_directory(self, path: str, types: t.Container[str] = ("file", "d
105137
log.debug(f"Fetching directory from branding repository: '{full_url}'.")
106138

107139
async with self.bot.http_session.get(full_url, params=PARAMS, headers=HEADERS) as response:
108-
if response.status != 200:
109-
raise RuntimeError(f"Failed to fetch directory due to status: {response.status}")
110-
111-
log.debug("Fetch successful, reading JSON response.")
140+
_raise_for_status(response)
112141
json_directory = await response.json()
113142

114143
return {file["name"]: RemoteObject(file) for file in json_directory if file["type"] in types}
115144

145+
@_retry_fetch
116146
async def fetch_file(self, download_url: str) -> bytes:
117147
"""
118148
Fetch file as bytes from `download_url`.
@@ -122,10 +152,7 @@ async def fetch_file(self, download_url: str) -> bytes:
122152
log.debug(f"Fetching file from branding repository: '{download_url}'.")
123153

124154
async with self.bot.http_session.get(download_url, params=PARAMS, headers=HEADERS) as response:
125-
if response.status != 200:
126-
raise RuntimeError(f"Failed to fetch file due to status: {response.status}")
127-
128-
log.debug("Fetch successful, reading payload.")
155+
_raise_for_status(response)
129156
return await response.read()
130157

131158
def parse_meta_file(self, raw_file: bytes) -> MetaFile:

0 commit comments

Comments
 (0)