From 77973d5573d74e5d77d788c96e6348461ff9c539 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Wed, 21 May 2025 18:16:39 -0600 Subject: [PATCH] refactor: Enhance database connection handling by prioritizing API server's pool and adding error handling --- api_service/api_server.py | 143 +++++++++++++++++++++---- api_service/dashboard_api_endpoints.py | 66 +++++++++++- settings_manager.py | 29 ++++- 3 files changed, 214 insertions(+), 24 deletions(-) diff --git a/api_service/api_server.py b/api_service/api_server.py index 676b155..673907b 100644 --- a/api_service/api_server.py +++ b/api_service/api_server.py @@ -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}") diff --git a/api_service/dashboard_api_endpoints.py b/api_service/dashboard_api_endpoints.py index 520e295..e31d894 100644 --- a/api_service/dashboard_api_endpoints.py +++ b/api_service/dashboard_api_endpoints.py @@ -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 diff --git a/settings_manager.py b/settings_manager.py index f283cb9..a424e4e 100644 --- a/settings_manager.py +++ b/settings_manager.py @@ -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