refactor: Enhance database connection handling by prioritizing API server's pool and adding error handling

This commit is contained in:
Slipstream 2025-05-21 18:16:39 -06:00
parent 172f5907b3
commit 77973d5573
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
3 changed files with 214 additions and 24 deletions

View File

@ -1704,12 +1704,13 @@ async def dashboard_get_user_guilds(current_user: dict = Depends(dependencies.ge
# Use the API server's own pool from app.state instead of the bot's pool
while retry_count < max_db_retries and bot_guild_ids is None:
try:
# Check if we have a pool in app.state
# Always use the API server's own pool with the new function
if hasattr(app.state, 'pg_pool') and app.state.pg_pool:
# Use the API server's own pool with the new function
log.info("Dashboard: Using API server's pool to fetch guild IDs")
bot_guild_ids = await settings_manager.get_bot_guild_ids_with_pool(app.state.pg_pool)
else:
# Fall back to the original function if app.state.pg_pool is not available
# The improved get_bot_guild_ids will try app.state.pg_pool first
log.info("Dashboard: Using enhanced get_bot_guild_ids that prioritizes API server's pool")
bot_guild_ids = await settings_manager.get_bot_guild_ids()
if bot_guild_ids is None:
@ -1717,6 +1718,31 @@ async def dashboard_get_user_guilds(current_user: dict = Depends(dependencies.ge
retry_count += 1
if retry_count < max_db_retries:
await asyncio.sleep(1) # Wait before retrying
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Dashboard: Event loop error fetching guild IDs: {e}")
log.warning("This is likely because we're trying to use a pool from a different thread.")
# Try to create a new pool just for this request if needed
if not hasattr(app.state, 'pg_pool') or not app.state.pg_pool:
try:
log.info("Dashboard: Attempting to create a temporary pool for this request")
temp_pool = await asyncpg.create_pool(
user=settings.POSTGRES_USER,
password=settings.POSTGRES_PASSWORD,
host=settings.POSTGRES_HOST,
database=settings.POSTGRES_SETTINGS_DB,
min_size=1,
max_size=2,
)
bot_guild_ids = await settings_manager.get_bot_guild_ids_with_pool(temp_pool)
await temp_pool.close()
except Exception as pool_err:
log.error(f"Dashboard: Failed to create temporary pool: {pool_err}")
else:
log.warning(f"Dashboard: Runtime error fetching bot guild IDs, retry {retry_count+1}/{max_db_retries}: {e}")
retry_count += 1
if retry_count < max_db_retries:
await asyncio.sleep(1) # Wait before retrying
except Exception as e:
log.warning(f"Dashboard: Error fetching bot guild IDs, retry {retry_count+1}/{max_db_retries}: {e}")
retry_count += 1
@ -2155,35 +2181,116 @@ async def dashboard_get_guild_settings(
known_cogs_in_db = {}
try:
# Need to acquire connection from pool
bot = get_bot_instance()
if bot and bot.pg_pool:
async with bot.pg_pool.acquire() as conn:
# First try to use the API server's pool
if hasattr(app.state, 'pg_pool') and app.state.pg_pool:
log.info(f"Dashboard: Using API server's pool to fetch cog statuses for guild {guild_id}")
async with app.state.pg_pool.acquire() as conn:
records = await conn.fetch("SELECT cog_name, enabled FROM enabled_cogs WHERE guild_id = $1", guild_id)
for record in records:
known_cogs_in_db[record['cog_name']] = record['enabled']
else:
log.error("Dashboard: Bot instance or pg_pool not initialized.")
# Decide how to handle - return empty or error?
# Fall back to bot's pool if API server pool not available
bot = get_bot_instance()
if bot and bot.pg_pool:
log.info(f"Dashboard: Using bot's pool to fetch cog statuses for guild {guild_id}")
async with bot.pg_pool.acquire() as conn:
records = await conn.fetch("SELECT cog_name, enabled FROM enabled_cogs WHERE guild_id = $1", guild_id)
for record in records:
known_cogs_in_db[record['cog_name']] = record['enabled']
else:
log.error("Dashboard: Neither API server pool nor bot pool is available")
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Dashboard: Event loop error fetching cog statuses: {e}")
log.warning("This is likely because we're trying to use a pool from a different thread.")
# Try to create a temporary pool just for this request
try:
log.info("Dashboard: Attempting to create a temporary pool for cog statuses")
temp_pool = await asyncpg.create_pool(
user=settings.POSTGRES_USER,
password=settings.POSTGRES_PASSWORD,
host=settings.POSTGRES_HOST,
database=settings.POSTGRES_SETTINGS_DB,
min_size=1,
max_size=2,
)
async with temp_pool.acquire() as conn:
records = await conn.fetch("SELECT cog_name, enabled FROM enabled_cogs WHERE guild_id = $1", guild_id)
for record in records:
known_cogs_in_db[record['cog_name']] = record['enabled']
await temp_pool.close()
except Exception as pool_err:
log.error(f"Dashboard: Failed to create temporary pool for cog statuses: {pool_err}")
else:
log.exception(f"Dashboard: Runtime error fetching cog statuses from DB for guild {guild_id}: {e}")
except Exception as e:
log.exception(f"Dashboard: Failed to fetch cog statuses from DB for guild {guild_id}: {e}")
# Fetch command permissions
permissions_map: Dict[str, List[str]] = {}
try:
bot = get_bot_instance()
if bot and bot.pg_pool:
async with bot.pg_pool.acquire() as conn:
# First try to use the API server's pool
if hasattr(app.state, 'pg_pool') and app.state.pg_pool:
log.info(f"Dashboard: Using API server's pool to fetch command permissions for guild {guild_id}")
async with app.state.pg_pool.acquire() as conn:
records = await conn.fetch(
"SELECT command_name, allowed_role_id FROM command_permissions WHERE guild_id = $1 ORDER BY command_name, allowed_role_id",
guild_id
)
for record in records:
cmd = record['command_name']
role_id_str = str(record['allowed_role_id'])
if cmd not in permissions_map:
permissions_map[cmd] = []
permissions_map[cmd].append(role_id_str)
for record in records:
cmd = record['command_name']
role_id_str = str(record['allowed_role_id'])
if cmd not in permissions_map:
permissions_map[cmd] = []
permissions_map[cmd].append(role_id_str)
else:
# Fall back to bot's pool if API server pool not available
bot = get_bot_instance()
if bot and bot.pg_pool:
log.info(f"Dashboard: Using bot's pool to fetch command permissions for guild {guild_id}")
async with bot.pg_pool.acquire() as conn:
records = await conn.fetch(
"SELECT command_name, allowed_role_id FROM command_permissions WHERE guild_id = $1 ORDER BY command_name, allowed_role_id",
guild_id
)
for record in records:
cmd = record['command_name']
role_id_str = str(record['allowed_role_id'])
if cmd not in permissions_map:
permissions_map[cmd] = []
permissions_map[cmd].append(role_id_str)
else:
log.error("Dashboard: Neither API server pool nor bot pool is available")
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Dashboard: Event loop error fetching command permissions: {e}")
# Try to create a temporary pool just for this request
try:
log.info("Dashboard: Attempting to create a temporary pool for command permissions")
temp_pool = await asyncpg.create_pool(
user=settings.POSTGRES_USER,
password=settings.POSTGRES_PASSWORD,
host=settings.POSTGRES_HOST,
database=settings.POSTGRES_SETTINGS_DB,
min_size=1,
max_size=2,
)
async with temp_pool.acquire() as conn:
records = await conn.fetch(
"SELECT command_name, allowed_role_id FROM command_permissions WHERE guild_id = $1 ORDER BY command_name, allowed_role_id",
guild_id
)
for record in records:
cmd = record['command_name']
role_id_str = str(record['allowed_role_id'])
if cmd not in permissions_map:
permissions_map[cmd] = []
permissions_map[cmd].append(role_id_str)
await temp_pool.close()
except Exception as pool_err:
log.error(f"Dashboard: Failed to create temporary pool for command permissions: {pool_err}")
else:
log.exception(f"Dashboard: Runtime error fetching command permissions from DB for guild {guild_id}: {e}")
except Exception as e:
log.exception(f"Dashboard: Failed to fetch command permissions from DB for guild {guild_id}: {e}")

View File

@ -621,14 +621,31 @@ async def get_guild_settings(
"""Get settings for a guild."""
try:
# Check if settings_manager is available
from global_bot_accessor import get_bot_instance
bot = get_bot_instance()
if not settings_manager or not bot or not bot.pg_pool:
if not settings_manager:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Settings manager or database connection not available"
detail="Settings manager not available"
)
# Try to get the API server's pool from FastAPI app
try:
from api_service.api_server import app
has_api_pool = hasattr(app, 'state') and hasattr(app.state, 'pg_pool') and app.state.pg_pool
log.info(f"API server pool available: {has_api_pool}")
except (ImportError, AttributeError):
has_api_pool = False
log.warning("Could not access API server pool")
# Check bot pool as fallback
from global_bot_accessor import get_bot_instance
bot = get_bot_instance()
has_bot_pool = bot and hasattr(bot, 'pg_pool') and bot.pg_pool
log.info(f"Bot pool available: {has_bot_pool}")
if not has_api_pool and not has_bot_pool:
log.warning("Neither API server pool nor bot pool is available")
# Continue anyway - the settings_manager functions will handle the missing pools
# Initialize settings with defaults
settings = {
"prefix": DEFAULT_PREFIX,
@ -643,6 +660,13 @@ async def get_guild_settings(
# Get prefix with error handling
try:
settings["prefix"] = await settings_manager.get_guild_prefix(guild_id, DEFAULT_PREFIX)
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Event loop error getting prefix for guild {guild_id}: {e}")
# Keep default prefix
else:
log.warning(f"Runtime error getting prefix for guild {guild_id}: {e}")
# Keep default prefix
except Exception as e:
log.warning(f"Error getting prefix for guild {guild_id}, using default: {e}")
# Keep default prefix
@ -650,27 +674,54 @@ async def get_guild_settings(
# Get welcome/goodbye settings with error handling
try:
settings["welcome_channel_id"] = await settings_manager.get_setting(guild_id, 'welcome_channel_id')
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Event loop error getting welcome_channel_id for guild {guild_id}: {e}")
else:
log.warning(f"Runtime error getting welcome_channel_id for guild {guild_id}: {e}")
except Exception as e:
log.warning(f"Error getting welcome_channel_id for guild {guild_id}: {e}")
try:
settings["welcome_message"] = await settings_manager.get_setting(guild_id, 'welcome_message')
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Event loop error getting welcome_message for guild {guild_id}: {e}")
else:
log.warning(f"Runtime error getting welcome_message for guild {guild_id}: {e}")
except Exception as e:
log.warning(f"Error getting welcome_message for guild {guild_id}: {e}")
try:
settings["goodbye_channel_id"] = await settings_manager.get_setting(guild_id, 'goodbye_channel_id')
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Event loop error getting goodbye_channel_id for guild {guild_id}: {e}")
else:
log.warning(f"Runtime error getting goodbye_channel_id for guild {guild_id}: {e}")
except Exception as e:
log.warning(f"Error getting goodbye_channel_id for guild {guild_id}: {e}")
try:
settings["goodbye_message"] = await settings_manager.get_setting(guild_id, 'goodbye_message')
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Event loop error getting goodbye_message for guild {guild_id}: {e}")
else:
log.warning(f"Runtime error getting goodbye_message for guild {guild_id}: {e}")
except Exception as e:
log.warning(f"Error getting goodbye_message for guild {guild_id}: {e}")
# Get cog enabled statuses with error handling
try:
settings["cogs"] = await settings_manager.get_all_enabled_cogs(guild_id)
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Event loop error getting cog enabled statuses for guild {guild_id}: {e}")
# Keep empty dict for cogs
else:
log.warning(f"Runtime error getting cog enabled statuses for guild {guild_id}: {e}")
# Keep empty dict for cogs
except Exception as e:
log.warning(f"Error getting cog enabled statuses for guild {guild_id}: {e}")
# Keep empty dict for cogs
@ -678,6 +729,13 @@ async def get_guild_settings(
# Get command enabled statuses with error handling
try:
settings["commands"] = await settings_manager.get_all_enabled_commands(guild_id)
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.warning(f"Event loop error getting command enabled statuses for guild {guild_id}: {e}")
# Keep empty dict for commands
else:
log.warning(f"Runtime error getting command enabled statuses for guild {guild_id}: {e}")
# Keep empty dict for commands
except Exception as e:
log.warning(f"Error getting command enabled statuses for guild {guild_id}: {e}")
# Keep empty dict for commands

View File

@ -1680,7 +1680,23 @@ async def get_bot_guild_ids() -> set[int] | None:
"""
Gets the set of all guild IDs known to the bot from the guilds table.
Returns None on error or if pool not initialized.
This function will first try to use the API server's pool if available,
and fall back to the bot's pool if not.
"""
# First, try to get the API server's pool from FastAPI app.state
try:
# Import here to avoid circular imports
from api_service.api_server import app
if hasattr(app, 'state') and hasattr(app.state, 'pg_pool') and app.state.pg_pool:
log.debug("Using API server's PostgreSQL pool for get_bot_guild_ids")
return await get_bot_guild_ids_with_pool(app.state.pg_pool)
except (ImportError, AttributeError) as e:
log.debug(f"API server pool not available, will try bot pool: {e}")
except Exception as e:
log.warning(f"Error accessing API server pool: {e}")
# Fall back to the bot's pool
bot = get_bot_instance()
if not bot or not bot.pg_pool:
log.error("Bot instance or PostgreSQL pool not available in settings_manager. Cannot get bot guild IDs.")
@ -1691,11 +1707,20 @@ async def get_bot_guild_ids() -> set[int] | None:
async with bot.pg_pool.acquire() as conn:
records = await conn.fetch("SELECT guild_id FROM guilds")
guild_ids = {record['guild_id'] for record in records}
log.debug(f"Fetched {len(guild_ids)} guild IDs from database using pool.")
log.debug(f"Fetched {len(guild_ids)} guild IDs from database using bot pool.")
return guild_ids
except asyncpg.exceptions.PostgresError as e:
log.exception(f"PostgreSQL error fetching bot guild IDs using pool: {e}")
log.exception(f"PostgreSQL error fetching bot guild IDs using bot pool: {e}")
return None
except RuntimeError as e:
if "got Future" in str(e) and "attached to a different loop" in str(e):
log.error(f"Event loop error in get_bot_guild_ids: {e}")
log.warning("This is likely because the function is being called from the API server thread.")
log.warning("Try using get_bot_guild_ids_with_pool with app.state.pg_pool instead.")
return None
else:
log.exception(f"Runtime error fetching bot guild IDs: {e}")
return None
except Exception as e:
log.exception(f"Unexpected error fetching bot guild IDs: {e}")
return None