give the db listener a dedicated connection that is closed on cog unload

The listener previously held a pool connection that was never released and
kept its callback registered after a reload, so every reload leaked a pool
connection and added another handler for each notification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
leanderandClaude Opus 5.5 committed 2026-10-23 02:42:05 +02:00
1 parent 13703c35a2
commit 346b4558bd
2 files changed
+43 -30

No files matched your search

+40 -28
View File
@@ -5,6 +5,7 @@ import secrets
import urllib.parse import urllib.parse
import aiohttp import aiohttp
import asyncpg
import discord import discord
from cogs.utils import discord_utils from cogs.utils import discord_utils
from cogs.utils import embed_templates from cogs.utils import embed_templates
@@ -30,15 +31,25 @@ class Galtinn(commands.Cog):
self.bot = bot self.bot = bot
self.listen_conn: asyncpg.Connection | None = None
async def cog_load(self):
await self.init_db()
await self.listen_db()
self.membership_check.start() self.membership_check.start()
self.verification_cleanup.start() self.verification_cleanup.start()
asyncio.create_task(self.listen_db())
def cog_unload(self): async def cog_unload(self):
self.bot.logger.info("Unloading cog") self.bot.logger.info("Unloading cog")
self.membership_check.cancel() self.membership_check.cancel()
self.verification_cleanup.cancel() self.verification_cleanup.cancel()
# Closing the connection also removes its listeners
if self.listen_conn:
await self.listen_conn.close()
self.listen_conn = None
async def init_db(self): async def init_db(self):
""" """
Create the necessary tables for the cog to work Create the necessary tables for the cog to work
@@ -76,30 +87,39 @@ class Galtinn(commands.Cog):
async def listen_db(self): async def listen_db(self):
""" """
Creates a listener for galitnn_auth_complete events from the database and processes them Opens a dedicated database connection that listens for galtinn_auth_complete events.
It is kept outside the pool so it doesn't hold a pool connection for the lifetime of the cog
""" """
async def process_notification(conn, pid, channel, payload): self.listen_conn = await asyncpg.connect(**self.bot.db_credentials)
if channel != "galtinn_auth_complete": await self.listen_conn.add_listener("galtinn_auth_complete", self.process_notification)
return
self.bot.logger.info("Received galtinn_auth_complete event from database") async def process_notification(self, conn: asyncpg.Connection, pid: int, channel: str, payload: str):
"""
Applies Galtinn roles to a user that has just completed verification
"""
try: self.bot.logger.info("Received galtinn_auth_complete event from database")
discord_user_id, galtinn_user_id = payload.split(" ", 1)
discord_user_id = int(discord_user_id)
galtinn_user_id = int(galtinn_user_id)
except (TypeError, ValueError):
self.bot.logger.error(f"Invalid galtinn_auth_complete payload: {payload!r}")
return
try:
discord_user_id, galtinn_user_id = payload.split(" ", 1)
discord_user_id = int(discord_user_id)
galtinn_user_id = int(galtinn_user_id)
except (TypeError, ValueError):
self.bot.logger.error(f"Invalid galtinn_auth_complete payload: {payload!r}")
return
# asyncpg runs the callback as a detached task, so exceptions would otherwise go unnoticed
try:
# Fetch Galtinn user # Fetch Galtinn user
if not ( if not (
galtinn_users := await self.fetch_galtinn_users( galtinn_users := await self.fetch_galtinn_users(
galtinn_user_id=galtinn_user_id, discord_id=discord_user_id galtinn_user_id=galtinn_user_id, discord_id=discord_user_id
) )
): ):
self.bot.logger.error(f"Failed to fetch Galtinn user {galtinn_user_id} for Discord user {discord_user_id}") self.bot.logger.error(
f"Failed to fetch Galtinn user {galtinn_user_id} for Discord user {discord_user_id}"
)
return return
galtinn_user = galtinn_users.results[0] galtinn_user = galtinn_users.results[0]
@@ -117,17 +137,11 @@ class Galtinn(commands.Cog):
if not await self.update_roles(discord_user, roles_to_add, roles_to_remove): if not await self.update_roles(discord_user, roles_to_add, roles_to_remove):
self.bot.logger.error(f"Failed to apply Galtinn roles for user {discord_user_id}") self.bot.logger.error(f"Failed to apply Galtinn roles for user {discord_user_id}")
return return
except Exception:
self.bot.logger.exception(f"Failed to process galtinn_auth_complete event for user {discord_user_id}")
return
self.bot.logger.info(f"Completed processing {discord_user_id}!") self.bot.logger.info(f"Completed processing {discord_user_id}!")
conn = await self.bot.db.acquire()
await conn.add_listener("galtinn_auth_complete", process_notification)
while True:
if not self.bot.get_cog("Galtinn"):
self.bot.logger.info("Galtinn cog unload detected. Stopping db listener")
break
await asyncio.sleep(1)
async def fetch_galtinn_users( async def fetch_galtinn_users(
self, galtinn_user_id: int | None = None, discord_id: int | None = None, page: int = 1 self, galtinn_user_id: int | None = None, discord_id: int | None = None, page: int = 1
@@ -714,6 +728,4 @@ async def setup(bot: commands.Bot):
bot (commands.Bot): Bot instance bot (commands.Bot): Bot instance
""" """
cog = Galtinn(bot) await bot.add_cog(Galtinn(bot))
await cog.init_db()
await bot.add_cog(cog)
+3 -2
View File
@@ -46,13 +46,14 @@ class Bot(commands.Bot):
async def setup_hook(self): async def setup_hook(self):
# DB needs to be setup here because contructor is not async # DB needs to be setup here because contructor is not async
# Not ideal but does the job # Not ideal but does the job
credentials = { # Credentials are kept for cogs that need a dedicated connection outside the pool
self.db_credentials = {
"host": os.environ["DATABASE_HOST"], "host": os.environ["DATABASE_HOST"],
"database": os.environ["DATABASE_NAME"], "database": os.environ["DATABASE_NAME"],
"user": os.environ["DATABASE_USER"], "user": os.environ["DATABASE_USER"],
"password": os.environ["DATABASE_PASSWORD"], "password": os.environ["DATABASE_PASSWORD"],
} }
self.db = await asyncpg.create_pool(**credentials) self.db = await asyncpg.create_pool(**self.db_credentials)
# Load cogs # Load cogs
cogs = os.listdir("./src/cogs") cogs = os.listdir("./src/cogs")