From aea645d874ee617181263d4a171097933e99b46e Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Sat, 24 Oct 2026 04:50:56 +0200 Subject: [PATCH] log background loop errors --- src/cogs/aurora.py | 3 +++ src/cogs/birthday.py | 1 + src/cogs/repeated_messages.py | 3 +++ src/cogs/social_credit.py | 1 + src/cogs/starboard_scraper.py | 1 + src/cogs/streak.py | 2 ++ src/cogs/utils/discord_utils.py | 2 ++ src/cogs/utils/misc_utils.py | 37 +++++++++++++++++++++++++++++++++ src/cogs/word_cloud.py | 2 ++ 9 files changed, 52 insertions(+) diff --git a/src/cogs/aurora.py b/src/cogs/aurora.py index cd54141..376a1b0 100644 --- a/src/cogs/aurora.py +++ b/src/cogs/aurora.py @@ -7,6 +7,8 @@ import requests from discord.ext import commands from discord.ext import tasks +from cogs.utils import misc_utils + class Aurora(commands.Cog): """Aurora Borealis forecasts and alerts""" @@ -71,6 +73,7 @@ class Aurora(commands.Cog): return most_likely_sighting @tasks.loop(minutes=60) + @misc_utils.log_errors async def aurora_alarm(self): """ Checks if the aurora forecast is above 50% and sends a message to the aurora channel if it is diff --git a/src/cogs/birthday.py b/src/cogs/birthday.py index 9fda735..821d031 100644 --- a/src/cogs/birthday.py +++ b/src/cogs/birthday.py @@ -47,6 +47,7 @@ class Birthday(commands.Cog): ) @tasks.loop(time=misc_utils.MIDNIGHT) + @misc_utils.log_errors async def birthday_check(self): """ Check if it's someone's birthday every day at midgnight and send a greeting if it is diff --git a/src/cogs/repeated_messages.py b/src/cogs/repeated_messages.py index 9f4436d..ff25132 100644 --- a/src/cogs/repeated_messages.py +++ b/src/cogs/repeated_messages.py @@ -24,6 +24,7 @@ class RepeatedMessages(commands.Cog): self.new_year.cancel() @tasks.loop(time=misc_utils.MIDNIGHT) + @misc_utils.log_errors async def fredag(self): """ Sends a message on Friday at 00:00 @@ -56,6 +57,7 @@ class RepeatedMessages(commands.Cog): await channel.send("NU ÄR DET FREDAG!\n" + video) @tasks.loop(time=misc_utils.MIDNIGHT) + @misc_utils.log_errors async def mandag(self): """ Sends a message on Monday at 00:00 @@ -71,6 +73,7 @@ class RepeatedMessages(commands.Cog): ) @tasks.loop(time=misc_utils.MIDNIGHT) + @misc_utils.log_errors async def new_year(self): """ Sends a message on New Year's Day at 00:00 diff --git a/src/cogs/social_credit.py b/src/cogs/social_credit.py index 9dc63b6..e453b5a 100644 --- a/src/cogs/social_credit.py +++ b/src/cogs/social_credit.py @@ -104,6 +104,7 @@ class SocialCredit(commands.Cog): ) @tasks.loop(time=misc_utils.MIDNIGHT, reconnect=True) + @misc_utils.log_errors async def fuck_uwu(self): """ Deducts points from people with the weeb role every 24 hours diff --git a/src/cogs/starboard_scraper.py b/src/cogs/starboard_scraper.py index 2999547..92fd9fe 100644 --- a/src/cogs/starboard_scraper.py +++ b/src/cogs/starboard_scraper.py @@ -188,6 +188,7 @@ class StarboardScraper(commands.Cog): self.bot.logger.error(f"Error during starboard scrape: {e}") @tasks.loop(time=misc_utils.MIDNIGHT) + @misc_utils.log_errors async def star_check(self): """ Update DB with latest starboard data diff --git a/src/cogs/streak.py b/src/cogs/streak.py index 89192cd..aa46f52 100644 --- a/src/cogs/streak.py +++ b/src/cogs/streak.py @@ -96,6 +96,7 @@ class Streak(commands.Cog): } @tasks.loop(minutes=10) + @misc_utils.log_errors async def insert_cache_loop(self): """ Inserts cache into the database every 10 minutes @@ -129,6 +130,7 @@ class Streak(commands.Cog): ) @tasks.loop(time=misc_utils.MIDNIGHT) + @misc_utils.log_errors async def streak_check(self): """ Check if anyone has lost their streak diff --git a/src/cogs/utils/discord_utils.py b/src/cogs/utils/discord_utils.py index fbae368..8c22046 100644 --- a/src/cogs/utils/discord_utils.py +++ b/src/cogs/utils/discord_utils.py @@ -9,6 +9,7 @@ from discord.ext import tasks from . import embed_templates from .misc_utils import Paginator +from .misc_utils import log_errors async def send_as_txt_file(interaction: discord.Interaction, content: str, file_path: str): @@ -387,6 +388,7 @@ class TempVoiceHelper: self.temp_vc_channels[channel]["no_members_since"] = None @tasks.loop(minutes=1) + @log_errors async def check_temp_vc_channels(self): """ Check for temporary voice channels that have been inactive for 1 minute diff --git a/src/cogs/utils/misc_utils.py b/src/cogs/utils/misc_utils.py index c369cff..142e472 100644 --- a/src/cogs/utils/misc_utils.py +++ b/src/cogs/utils/misc_utils.py @@ -1,9 +1,13 @@ +import asyncio import datetime +import functools import textwrap from math import ceil from zoneinfo import ZoneInfo +import aiohttp import cv2 +import discord import numpy as np from PIL import Image from PIL import ImageDraw @@ -12,6 +16,39 @@ from PIL import ImageFont MIDNIGHT = datetime.time(hour=0, minute=0, tzinfo=ZoneInfo("Europe/Oslo")) +# The exceptions discord.py's tasks.loop retries instead of stopping on +RETRIED_TASK_EXCEPTIONS = ( + OSError, + discord.GatewayNotFound, + discord.ConnectionClosed, + aiohttp.ClientError, + asyncio.TimeoutError, +) + + +def log_errors(func): + """ + Decorator for background task loops that logs exceptions instead of letting them stop the loop. + Network errors are re-raised so discord.py can retry the iteration like it normally does. + Put it below the @tasks.loop decorator. The decorated method's class needs a `bot` attribute + + Parameters + ---------- + func (callable): The loop's coroutine function + """ + + @functools.wraps(func) + async def wrapper(self, *args, **kwargs): + try: + await func(self, *args, **kwargs) + except RETRIED_TASK_EXCEPTIONS: + raise + except Exception: + self.bot.logger.exception(f"Unhandled error in background task {func.__qualname__}") + + return wrapper + + class Paginator: """Interface for managing content. Divides your content into pages of 10 items each""" diff --git a/src/cogs/word_cloud.py b/src/cogs/word_cloud.py index aba7737..cf48cc5 100644 --- a/src/cogs/word_cloud.py +++ b/src/cogs/word_cloud.py @@ -19,6 +19,7 @@ from psycopg2.extras import execute_batch from wordcloud import WordCloud as WCloud # Avoid naming conflicts with cog class name from cogs.utils import embed_templates +from cogs.utils import misc_utils class WordCloud(commands.Cog): @@ -128,6 +129,7 @@ class WordCloud(commands.Cog): self.word_freq_cache.clear() @tasks.loop(minutes=20) + @misc_utils.log_errors async def insert_cache_loop(self): """ Clears and inserts word frequency cache into the database every 20 minutes