From 9585e06397750d21c7148ef939b1397ac963edc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:11:33 +0000 Subject: [PATCH 1/7] Initial plan From 8abd6b4daa47bfa6f2e5b4bd97fa47d421926a7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:18:15 +0000 Subject: [PATCH 2/7] Fix performance issues: reuse HTTP clients and optimize config reads Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- activity_stats/activity_stats.py | 24 ++++---- albion_auth/auth.py | 65 ++++++++++++++------ albion_regear/regear.py | 74 ++++++++++++++++------- movie_vote/movie_vote.py | 98 +++++++++++++++++++++++-------- nw_server_status/server_status.py | 56 ++++++++++++------ tgmc/api.py | 55 ++++++++++++----- 6 files changed, 265 insertions(+), 107 deletions(-) diff --git a/activity_stats/activity_stats.py b/activity_stats/activity_stats.py index 10c2eba..de3f45d 100644 --- a/activity_stats/activity_stats.py +++ b/activity_stats/activity_stats.py @@ -81,27 +81,29 @@ async def _stop_tracking_game(self, guild_config, user_id: int, game_name: str, duration = timestamp - start_time if duration > 0: - # Update global game stats - async with guild_config.game_stats() as game_stats: + # Batch all config updates in a single context manager access + async with guild_config.all() as all_data: + # Update global game stats + game_stats = all_data.setdefault("game_stats", {}) if game_name not in game_stats: game_stats[game_name] = 0 game_stats[game_name] += duration - # Update user game stats - async with guild_config.user_game_stats() as user_game_stats: + # Update user game stats + user_game_stats = all_data.setdefault("user_game_stats", {}) if user_id_str not in user_game_stats: user_game_stats[user_id_str] = {} if game_name not in user_game_stats[user_id_str]: user_game_stats[user_id_str][game_name] = 0 user_game_stats[user_id_str][game_name] += duration - # Remove from last_activity - async with guild_config.last_activity() as last_activity_update: - if user_id_str in last_activity_update and game_name in last_activity_update[user_id_str]: - del last_activity_update[user_id_str][game_name] - # Clean up empty user entries - if not last_activity_update[user_id_str]: - del last_activity_update[user_id_str] + # Remove from last_activity + last_activity_update = all_data.setdefault("last_activity", {}) + if user_id_str in last_activity_update and game_name in last_activity_update[user_id_str]: + del last_activity_update[user_id_str][game_name] + # Clean up empty user entries + if not last_activity_update[user_id_str]: + del last_activity_update[user_id_str] @commands.guild_only() @commands.group(name="activity", invoke_without_command=True) diff --git a/albion_auth/auth.py b/albion_auth/auth.py index d6cae3c..d46d035 100644 --- a/albion_auth/auth.py +++ b/albion_auth/auth.py @@ -10,32 +10,54 @@ log = logging.getLogger("red.cogs.albion_auth") -async def http_get(url, params=None): - """Make HTTP GET request with retries""" +async def http_get(url, params=None, client=None): + """Make HTTP GET request with retries + + Args: + url: URL to fetch + params: Query parameters + client: Optional httpx.AsyncClient to reuse. If None, creates a new one. + """ max_attempts = 3 attempt = 0 log.info(f"Making HTTP GET request to {url} with params: {params}") - while attempt < max_attempts: - try: - async with httpx.AsyncClient() as client: + + # Create client if not provided + should_close = client is None + if should_close: + client = httpx.AsyncClient() + + try: + while attempt < max_attempts: + try: r = await client.get(url, params=params, timeout=10.0) - if r.status_code == 200: - response_data = r.json() - log.info(f"HTTP GET successful for {url} - Status: {r.status_code}") - log.debug(f"Response data: {response_data}") - return response_data - else: + if r.status_code == 200: + response_data = r.json() + log.info(f"HTTP GET successful for {url} - Status: {r.status_code}") + log.debug(f"Response data: {response_data}") + return response_data + else: + attempt += 1 + log.warning( + f"HTTP GET failed for {url} - Status: {r.status_code}, " + f"Attempt {attempt}/{max_attempts}" + ) + await asyncio.sleep(2) + except (httpx.ConnectTimeout, httpx.RequestError) as e: attempt += 1 - log.warning(f"HTTP GET failed for {url} - Status: {r.status_code}, Attempt {attempt}/{max_attempts}") + log.warning( + f"HTTP GET error for {url}: {type(e).__name__}: {str(e)}, " + f"Attempt {attempt}/{max_attempts}" + ) await asyncio.sleep(2) - except (httpx.ConnectTimeout, httpx.RequestError) as e: - attempt += 1 - log.warning(f"HTTP GET error for {url}: {type(e).__name__}: {str(e)}, Attempt {attempt}/{max_attempts}") - await asyncio.sleep(2) - log.error(f"HTTP GET failed after {max_attempts} attempts for {url}") - return None + log.error(f"HTTP GET failed after {max_attempts} attempts for {url}") + return None + finally: + # Only close if we created it + if should_close: + await client.aclose() class AlbionAuth(commands.Cog): @@ -51,9 +73,11 @@ def __init__(self, bot): enable_daily_check=True ) self._check_task = None + self._http_client = None async def cog_load(self): """Start the background task when cog loads""" + self._http_client = httpx.AsyncClient() self._check_task = self.bot.loop.create_task(self._daily_check_loop()) log.info("Started daily name check task") @@ -62,12 +86,15 @@ async def cog_unload(self): if self._check_task: self._check_task.cancel() log.info("Cancelled daily name check task") + if self._http_client: + await self._http_client.aclose() + log.info("Closed HTTP client") async def search_player_in_region(self, name, region_url, region_name): """Search for a player by name in a specific region""" log.info(f"Searching for player '{name}' in {region_name} region") params = {"q": name} - result = await http_get(region_url, params) + result = await http_get(region_url, params, client=self._http_client) if result and result.get("players"): player = result["players"][0] diff --git a/albion_regear/regear.py b/albion_regear/regear.py index 3a77ebd..0ccf18b 100644 --- a/albion_regear/regear.py +++ b/albion_regear/regear.py @@ -8,32 +8,54 @@ log = logging.getLogger("red.cogs.albion_regear") -async def http_get(url, params=None): - """Make HTTP GET request with retries""" +async def http_get(url, params=None, client=None): + """Make HTTP GET request with retries + + Args: + url: URL to fetch + params: Query parameters + client: Optional httpx.AsyncClient to reuse. If None, creates a new one. + """ max_attempts = 3 attempt = 0 log.info(f"Making HTTP GET request to {url} with params: {params}") - while attempt < max_attempts: - try: - async with httpx.AsyncClient() as client: + + # Create client if not provided + should_close = client is None + if should_close: + client = httpx.AsyncClient() + + try: + while attempt < max_attempts: + try: r = await client.get(url, params=params, timeout=10.0) - if r.status_code == 200: - response_data = r.json() - log.info(f"HTTP GET successful for {url} - Status: {r.status_code}") - log.debug(f"Response data: {response_data}") - return response_data - else: + if r.status_code == 200: + response_data = r.json() + log.info(f"HTTP GET successful for {url} - Status: {r.status_code}") + log.debug(f"Response data: {response_data}") + return response_data + else: + attempt += 1 + log.warning( + f"HTTP GET failed for {url} - Status: {r.status_code}, " + f"Attempt {attempt}/{max_attempts}" + ) + await asyncio.sleep(2) + except (httpx.ConnectTimeout, httpx.RequestError) as e: attempt += 1 - log.warning(f"HTTP GET failed for {url} - Status: {r.status_code}, Attempt {attempt}/{max_attempts}") + log.warning( + f"HTTP GET error for {url}: {type(e).__name__}: {str(e)}, " + f"Attempt {attempt}/{max_attempts}" + ) await asyncio.sleep(2) - except (httpx.ConnectTimeout, httpx.RequestError) as e: - attempt += 1 - log.warning(f"HTTP GET error for {url}: {type(e).__name__}: {str(e)}, Attempt {attempt}/{max_attempts}") - await asyncio.sleep(2) - log.error(f"HTTP GET failed after {max_attempts} attempts for {url}") - return None + log.error(f"HTTP GET failed after {max_attempts} attempts for {url}") + return None + finally: + # Only close if we created it + if should_close: + await client.aclose() class AlbionRegear(commands.Cog): @@ -41,6 +63,16 @@ class AlbionRegear(commands.Cog): def __init__(self, bot): self.bot = bot + self._http_client = None + + async def cog_load(self): + """Initialize HTTP client when cog loads""" + self._http_client = httpx.AsyncClient() + + async def cog_unload(self): + """Close HTTP client when cog unloads""" + if self._http_client: + await self._http_client.aclose() def normalize_quality(self, quality): """Normalize quality value for price lookups @@ -81,7 +113,7 @@ async def search_player(self, name): log.info(f"Searching for player: {name}") url = "https://gameinfo-ams.albiononline.com/api/gameinfo/search" params = {"q": name} - result = await http_get(url, params) + result = await http_get(url, params, client=self._http_client) if result and result.get("players"): player = result["players"][0] @@ -95,7 +127,7 @@ async def get_latest_death(self, player_id): """Get the latest death event for a player""" log.info(f"Fetching latest death for player ID: {player_id}") url = f"https://gameinfo-ams.albiononline.com/api/gameinfo/players/{player_id}/deaths" - result = await http_get(url) + result = await http_get(url, client=self._http_client) if result and len(result) > 0: death = result[0] @@ -127,7 +159,7 @@ async def get_item_prices(self, items_with_quality): item_list = ",".join(items_with_quality.keys()) log.info(f"Fetching prices for {len(items_with_quality)} items: {list(items_with_quality.keys())}") url = f"https://europe.albion-online-data.com/api/v2/stats/prices/{item_list}?locations=Bridgewatch" - result = await http_get(url) + result = await http_get(url, client=self._http_client) if not result: log.error("Failed to fetch item prices - API returned no data") diff --git a/movie_vote/movie_vote.py b/movie_vote/movie_vote.py index f300391..adce4b9 100644 --- a/movie_vote/movie_vote.py +++ b/movie_vote/movie_vote.py @@ -31,6 +31,16 @@ def __init__(self, bot): "notify_episode": [], } self.config.register_guild(**default_guild) + self._http_client = None + + async def cog_load(self): + """Initialize HTTP client when cog loads""" + self._http_client = httpx.AsyncClient() + + async def cog_unload(self): + """Close HTTP client when cog unloads""" + if self._http_client: + await self._http_client.aclose() async def red_delete_data_for_user(self, **kwargs): """Nothing to delete.""" @@ -39,7 +49,8 @@ async def red_delete_data_for_user(self, **kwargs): async def get_latest_episodes(self, imdb_id: str) -> Union[Dict[str, Any], None]: """Get the latest episodes from vidsrc""" response = await http_get( - "https://vidsrc.me/episodes/latest/page-1.json" + "https://vidsrc.me/episodes/latest/page-1.json", + client=self._http_client ) if not response: log.info("Response was empty. %s", response) @@ -421,13 +432,25 @@ async def on_raw_reaction_add(self, payload): if not await self._is_movie_channel(payload): return - channel = await self.bot.fetch_channel(payload.channel_id) + # Ignore bot's own reactions + if payload.user_id == self.bot.user.id: + return + + # Use get methods first (cached), fallback to fetch only if needed + channel = self.bot.get_channel(payload.channel_id) + if not channel: + channel = await self.bot.fetch_channel(payload.channel_id) + + # Try to get message from cache first + message = channel.get_partial_message(payload.message_id) + # We need to fetch to get reactions message = await channel.fetch_message(payload.message_id) - user = await self.bot.fetch_user(payload.user_id) - emoji = payload.emoji - if user.id == self.bot.user.id: - return + user = self.bot.get_user(payload.user_id) + if not user: + user = await self.bot.fetch_user(payload.user_id) + + emoji = payload.emoji log.info(f"Reaction added. {user.name} on '{message.clean_content}'") await self.count_votes(message, emoji) @@ -438,13 +461,25 @@ async def on_raw_reaction_remove(self, payload): if not await self._is_movie_channel(payload): return - channel = await self.bot.fetch_channel(payload.channel_id) + # Ignore bot's own reactions + if payload.user_id == self.bot.user.id: + return + + # Use get methods first (cached), fallback to fetch only if needed + channel = self.bot.get_channel(payload.channel_id) + if not channel: + channel = await self.bot.fetch_channel(payload.channel_id) + + # Try to get message from cache first + message = channel.get_partial_message(payload.message_id) + # We need to fetch to get reactions message = await channel.fetch_message(payload.message_id) - user = await self.bot.fetch_user(payload.user_id) - emoji = payload.emoji - if user.id == self.bot.user.id: - return + user = self.bot.get_user(payload.user_id) + if not user: + user = await self.bot.fetch_user(payload.user_id) + + emoji = payload.emoji log.info(f"Reaction removed. {user.name} on '{message.clean_content}'") await self.count_votes(message, emoji) @@ -578,20 +613,35 @@ def fix_custom_emoji(self, emoji): return None -async def http_get(url): +async def http_get(url, client=None): + """Make HTTP GET request with retries + + Args: + url: URL to fetch + client: Optional httpx.AsyncClient to reuse. If None, creates a new one. + """ max_attempts = 3 attempt = 0 - while ( - max_attempts > attempt - ): # httpx doesn't support retries, so we'll build our own basic loop for that - try: - async with httpx.AsyncClient() as client: + + # Create client if not provided + should_close = client is None + if should_close: + client = httpx.AsyncClient() + + try: + while max_attempts > attempt: + try: r = await client.get(url, headers={"user-agent": "psykzz-cogs/1.0.0"}) - if r.status_code == 200: - return r.json() - else: + if r.status_code == 200: + return r.json() + else: + attempt += 1 + await asyncio.sleep(1) + except (httpx._exceptions.ConnectTimeout, httpx._exceptions.HTTPError): attempt += 1 - await asyncio.sleep(1) - except (httpx._exceptions.ConnectTimeout, httpx._exceptions.HTTPError): - attempt += 1 - await asyncio.sleep(1) + await asyncio.sleep(1) + return None + finally: + # Only close if we created it + if should_close: + await client.aclose() diff --git a/nw_server_status/server_status.py b/nw_server_status/server_status.py index 10a6de8..2415e98 100644 --- a/nw_server_status/server_status.py +++ b/nw_server_status/server_status.py @@ -31,16 +31,23 @@ def __init__(self, bot): self, identifier=IDENTIFIER, force_registration=True ) self.config.register_guild(**default_guild) + self._http_client = None self.refresh_queue_data.start() def cog_unload(self): self.refresh_queue_data.cancel() + if self._http_client: + asyncio.create_task(self._http_client.aclose()) @tasks.loop(minutes=5.0) async def refresh_queue_data(self): logger.info("Starting queue task") try: + # Initialize HTTP client on first run + if self._http_client is None: + self._http_client = httpx.AsyncClient() + self.queue_data = await self.get_queue_data(worldId=None) await self.update_monitor_channels() except Exception: @@ -52,9 +59,10 @@ async def get_queue_data(self, worldId=ishtakar_world_id): try: extra_qs = f"worldId={worldId}" if worldId else "" response = await http_get( - f"https://nwdb.info/server-status/servers.json?{extra_qs}" + f"https://nwdb.info/server-status/servers.json?{extra_qs}", + client=self._http_client ) - if not response.get("success"): + if not response or not response.get("success"): logger.error("Failed to get server status data") return servers = response.get("data", {}).get("servers", []) @@ -131,7 +139,7 @@ async def update_guild_channel(self, guild): async def update_monitor_channels(self): # iterate through bot discords and get the guild config for guild in self.bot.guilds: - self.update_guild_channel(guild) + await self.update_guild_channel(guild) async def get_server_status(self, server_name, data=None): if not data: @@ -236,21 +244,35 @@ async def queueset(self, ctx, server: str = None): await ctx.send(f"Server updated to '{server}'.") -async def http_get(url): +async def http_get(url, client=None): + """Make HTTP GET request with retries + + Args: + url: URL to fetch + client: Optional httpx.AsyncClient to reuse. If None, creates a new one. + """ max_attempts = 3 attempt = 0 - while ( - max_attempts > attempt - ): # httpx doesn't support retries, so we'll build our own basic loop for that - try: - async with httpx.AsyncClient() as client: + + # Create client if not provided + should_close = client is None + if should_close: + client = httpx.AsyncClient() + + try: + while max_attempts > attempt: + try: r = await client.get(url, headers={"user-agent": "psykzz-cogs/1.0.0"}) - if r.status_code == 200: - return r.json() - else: + if r.status_code == 200: + return r.json() + else: + attempt += 1 + await asyncio.sleep(5) + except (httpx._exceptions.ConnectTimeout, httpx._exceptions.HTTPError): attempt += 1 - await asyncio.sleep(5) - except (httpx._exceptions.ConnectTimeout, httpx._exceptions.HTTPError): - attempt += 1 - await asyncio.sleep(5) - pass + await asyncio.sleep(5) + return None + finally: + # Only close if we created it + if should_close: + await client.aclose() diff --git a/tgmc/api.py b/tgmc/api.py index 720716c..e1bc513 100644 --- a/tgmc/api.py +++ b/tgmc/api.py @@ -12,34 +12,59 @@ SOM_MINOR_VICTORY = "Sons of Mars Minor Victory" -async def http_get(url): +async def http_get(url, client=None): + """Make HTTP GET request with retries + + Args: + url: URL to fetch + client: Optional httpx.AsyncClient to reuse. If None, creates a new one. + """ max_attempts = 3 attempt = 0 - while ( - max_attempts > attempt - ): # httpx doesn't support retries, so we'll build our own basic loop for that - try: - async with httpx.AsyncClient() as client: + + # Create client if not provided + should_close = client is None + if should_close: + client = httpx.AsyncClient() + + try: + while max_attempts > attempt: + try: r = await client.get(url) - if r.status_code == 200: - return r.json() - else: + if r.status_code == 200: + return r.json() + else: + attempt += 1 + await asyncio.sleep(5) + except (httpx._exceptions.ConnectTimeout, httpx._exceptions.HTTPError): attempt += 1 - await asyncio.sleep(5) - except (httpx._exceptions.ConnectTimeout, httpx._exceptions.HTTPError): - attempt += 1 - await asyncio.sleep(5) - pass + await asyncio.sleep(5) + return None + finally: + # Only close if we created it + if should_close: + await client.aclose() class TGMC(commands.Cog): def __init__(self, bot): self.bot = bot + self._http_client = None + + async def cog_load(self): + """Initialize HTTP client when cog loads""" + self._http_client = httpx.AsyncClient() + + async def cog_unload(self): + """Close HTTP client when cog unloads""" + if self._http_client: + await self._http_client.aclose() async def get_winrate(self, ctx, delta="14", gamemode=None, custom_conditions=None): raw_data = await http_get( - f"https://statbus.psykzz.com/api/winrate?delta={delta}" + f"https://statbus.psykzz.com/api/winrate?delta={delta}", + client=self._http_client ) if not raw_data: return await ctx.send( From 49b2876d0e2da2c47ca621abc63c704b835daf4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:19:59 +0000 Subject: [PATCH 3/7] Address code review feedback: remove redundant code and fix async cog_unload Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- movie_vote/movie_vote.py | 8 ++------ nw_server_status/server_status.py | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/movie_vote/movie_vote.py b/movie_vote/movie_vote.py index adce4b9..a287802 100644 --- a/movie_vote/movie_vote.py +++ b/movie_vote/movie_vote.py @@ -441,9 +441,7 @@ async def on_raw_reaction_add(self, payload): if not channel: channel = await self.bot.fetch_channel(payload.channel_id) - # Try to get message from cache first - message = channel.get_partial_message(payload.message_id) - # We need to fetch to get reactions + # Fetch message to get reactions message = await channel.fetch_message(payload.message_id) user = self.bot.get_user(payload.user_id) @@ -470,9 +468,7 @@ async def on_raw_reaction_remove(self, payload): if not channel: channel = await self.bot.fetch_channel(payload.channel_id) - # Try to get message from cache first - message = channel.get_partial_message(payload.message_id) - # We need to fetch to get reactions + # Fetch message to get reactions message = await channel.fetch_message(payload.message_id) user = self.bot.get_user(payload.user_id) diff --git a/nw_server_status/server_status.py b/nw_server_status/server_status.py index 2415e98..e44bab7 100644 --- a/nw_server_status/server_status.py +++ b/nw_server_status/server_status.py @@ -35,10 +35,10 @@ def __init__(self, bot): self.refresh_queue_data.start() - def cog_unload(self): + async def cog_unload(self): self.refresh_queue_data.cancel() if self._http_client: - asyncio.create_task(self._http_client.aclose()) + await self._http_client.aclose() @tasks.loop(minutes=5.0) async def refresh_queue_data(self): From e02fcfee0d1dc461b44154fff095495c49b27870 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:21:33 +0000 Subject: [PATCH 4/7] Fix exception handling and sleep placement in HTTP retry logic Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- movie_vote/movie_vote.py | 4 ++-- nw_server_status/server_status.py | 4 ++-- tgmc/api.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/movie_vote/movie_vote.py b/movie_vote/movie_vote.py index a287802..ff8255d 100644 --- a/movie_vote/movie_vote.py +++ b/movie_vote/movie_vote.py @@ -632,8 +632,8 @@ async def http_get(url, client=None): return r.json() else: attempt += 1 - await asyncio.sleep(1) - except (httpx._exceptions.ConnectTimeout, httpx._exceptions.HTTPError): + await asyncio.sleep(1) + except (httpx.ConnectTimeout, httpx.HTTPError): attempt += 1 await asyncio.sleep(1) return None diff --git a/nw_server_status/server_status.py b/nw_server_status/server_status.py index e44bab7..b9ca363 100644 --- a/nw_server_status/server_status.py +++ b/nw_server_status/server_status.py @@ -267,8 +267,8 @@ async def http_get(url, client=None): return r.json() else: attempt += 1 - await asyncio.sleep(5) - except (httpx._exceptions.ConnectTimeout, httpx._exceptions.HTTPError): + await asyncio.sleep(5) + except (httpx.ConnectTimeout, httpx.HTTPError): attempt += 1 await asyncio.sleep(5) return None diff --git a/tgmc/api.py b/tgmc/api.py index e1bc513..611617f 100644 --- a/tgmc/api.py +++ b/tgmc/api.py @@ -36,8 +36,8 @@ async def http_get(url, client=None): return r.json() else: attempt += 1 - await asyncio.sleep(5) - except (httpx._exceptions.ConnectTimeout, httpx._exceptions.HTTPError): + await asyncio.sleep(5) + except (httpx.ConnectTimeout, httpx.HTTPError): attempt += 1 await asyncio.sleep(5) return None From 8d43738a29236ad2a0107422ec2827c03a9cf517 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:23:17 +0000 Subject: [PATCH 5/7] Improve consistency: use RequestError and add cog_load to nw_server_status Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- movie_vote/movie_vote.py | 2 +- nw_server_status/server_status.py | 10 +++++----- tgmc/api.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/movie_vote/movie_vote.py b/movie_vote/movie_vote.py index ff8255d..1db4fae 100644 --- a/movie_vote/movie_vote.py +++ b/movie_vote/movie_vote.py @@ -633,7 +633,7 @@ async def http_get(url, client=None): else: attempt += 1 await asyncio.sleep(1) - except (httpx.ConnectTimeout, httpx.HTTPError): + except (httpx.ConnectTimeout, httpx.RequestError): attempt += 1 await asyncio.sleep(1) return None diff --git a/nw_server_status/server_status.py b/nw_server_status/server_status.py index b9ca363..4545cf5 100644 --- a/nw_server_status/server_status.py +++ b/nw_server_status/server_status.py @@ -35,6 +35,10 @@ def __init__(self, bot): self.refresh_queue_data.start() + async def cog_load(self): + """Initialize HTTP client when cog loads""" + self._http_client = httpx.AsyncClient() + async def cog_unload(self): self.refresh_queue_data.cancel() if self._http_client: @@ -44,10 +48,6 @@ async def cog_unload(self): async def refresh_queue_data(self): logger.info("Starting queue task") try: - # Initialize HTTP client on first run - if self._http_client is None: - self._http_client = httpx.AsyncClient() - self.queue_data = await self.get_queue_data(worldId=None) await self.update_monitor_channels() except Exception: @@ -268,7 +268,7 @@ async def http_get(url, client=None): else: attempt += 1 await asyncio.sleep(5) - except (httpx.ConnectTimeout, httpx.HTTPError): + except (httpx.ConnectTimeout, httpx.RequestError): attempt += 1 await asyncio.sleep(5) return None diff --git a/tgmc/api.py b/tgmc/api.py index 611617f..a010736 100644 --- a/tgmc/api.py +++ b/tgmc/api.py @@ -37,7 +37,7 @@ async def http_get(url, client=None): else: attempt += 1 await asyncio.sleep(5) - except (httpx.ConnectTimeout, httpx.HTTPError): + except (httpx.ConnectTimeout, httpx.RequestError): attempt += 1 await asyncio.sleep(5) return None From 9f31665cfce9806abd200e6bf3b263083b9d14df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:25:07 +0000 Subject: [PATCH 6/7] Fix race condition and add null checks for channel fetches Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- movie_vote/movie_vote.py | 8 ++++++++ nw_server_status/server_status.py | 6 +----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/movie_vote/movie_vote.py b/movie_vote/movie_vote.py index 1db4fae..b815a22 100644 --- a/movie_vote/movie_vote.py +++ b/movie_vote/movie_vote.py @@ -441,6 +441,10 @@ async def on_raw_reaction_add(self, payload): if not channel: channel = await self.bot.fetch_channel(payload.channel_id) + if not channel: + log.warning(f"Channel {payload.channel_id} not found for reaction add event") + return + # Fetch message to get reactions message = await channel.fetch_message(payload.message_id) @@ -468,6 +472,10 @@ async def on_raw_reaction_remove(self, payload): if not channel: channel = await self.bot.fetch_channel(payload.channel_id) + if not channel: + log.warning(f"Channel {payload.channel_id} not found for reaction remove event") + return + # Fetch message to get reactions message = await channel.fetch_message(payload.message_id) diff --git a/nw_server_status/server_status.py b/nw_server_status/server_status.py index 4545cf5..2a0db23 100644 --- a/nw_server_status/server_status.py +++ b/nw_server_status/server_status.py @@ -31,14 +31,10 @@ def __init__(self, bot): self, identifier=IDENTIFIER, force_registration=True ) self.config.register_guild(**default_guild) - self._http_client = None + self._http_client = httpx.AsyncClient() self.refresh_queue_data.start() - async def cog_load(self): - """Initialize HTTP client when cog loads""" - self._http_client = httpx.AsyncClient() - async def cog_unload(self): self.refresh_queue_data.cancel() if self._http_client: From bd0fa10f6c983488f4bb73242ec8bda2d38561d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:27:41 +0000 Subject: [PATCH 7/7] Simplify HTTP client initialization across all cogs Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- albion_auth/auth.py | 3 +-- albion_regear/regear.py | 4 ---- movie_vote/movie_vote.py | 4 ---- tgmc/api.py | 4 ---- 4 files changed, 1 insertion(+), 14 deletions(-) diff --git a/albion_auth/auth.py b/albion_auth/auth.py index d46d035..deb9c3f 100644 --- a/albion_auth/auth.py +++ b/albion_auth/auth.py @@ -73,11 +73,10 @@ def __init__(self, bot): enable_daily_check=True ) self._check_task = None - self._http_client = None + self._http_client = httpx.AsyncClient() async def cog_load(self): """Start the background task when cog loads""" - self._http_client = httpx.AsyncClient() self._check_task = self.bot.loop.create_task(self._daily_check_loop()) log.info("Started daily name check task") diff --git a/albion_regear/regear.py b/albion_regear/regear.py index 0ccf18b..e8b6f4e 100644 --- a/albion_regear/regear.py +++ b/albion_regear/regear.py @@ -63,10 +63,6 @@ class AlbionRegear(commands.Cog): def __init__(self, bot): self.bot = bot - self._http_client = None - - async def cog_load(self): - """Initialize HTTP client when cog loads""" self._http_client = httpx.AsyncClient() async def cog_unload(self): diff --git a/movie_vote/movie_vote.py b/movie_vote/movie_vote.py index b815a22..728da1a 100644 --- a/movie_vote/movie_vote.py +++ b/movie_vote/movie_vote.py @@ -31,10 +31,6 @@ def __init__(self, bot): "notify_episode": [], } self.config.register_guild(**default_guild) - self._http_client = None - - async def cog_load(self): - """Initialize HTTP client when cog loads""" self._http_client = httpx.AsyncClient() async def cog_unload(self): diff --git a/tgmc/api.py b/tgmc/api.py index a010736..d7ae4dd 100644 --- a/tgmc/api.py +++ b/tgmc/api.py @@ -50,10 +50,6 @@ async def http_get(url, client=None): class TGMC(commands.Cog): def __init__(self, bot): self.bot = bot - self._http_client = None - - async def cog_load(self): - """Initialize HTTP client when cog loads""" self._http_client = httpx.AsyncClient() async def cog_unload(self):