From e8bd48da113d75654ac9e8f4b26de85a3cd9d42b Mon Sep 17 00:00:00 2001 From: Slipstream Date: Sat, 3 May 2025 18:51:44 -0600 Subject: [PATCH] aa --- api_service/api_server.py | 62 ++++++++++++++++++++++++++++++----- restart_api.bat | 15 +++++++++ restart_api.py | 69 +++++++++++++++++++++++++++++++++++++++ settings_manager.py | 64 ++++++++++++++++++++++++++++++------ 4 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 restart_api.bat create mode 100644 restart_api.py diff --git a/api_service/api_server.py b/api_service/api_server.py index 73f284f..f36a11f 100644 --- a/api_service/api_server.py +++ b/api_service/api_server.py @@ -156,13 +156,38 @@ async def lifespan(_: FastAPI): # Underscore indicates unused but required para # than the main bot, so it needs its own connection pools if settings_manager: log.info("Initializing database and cache connection pools for API server...") - try: - # Initialize the pools in the settings_manager module - await settings_manager.initialize_pools() - log.info("Database and cache connection pools initialized for API server.") - except Exception as e: - log.exception(f"Failed to initialize connection pools for API server: {e}") - log.error("Dashboard endpoints requiring DB/cache will fail.") + + # Add retry logic for database initialization + max_retries = 3 + retry_count = 0 + success = False + + while retry_count < max_retries and not success: + try: + # Initialize the pools in the settings_manager module + # Close any existing pools first to ensure clean state + if settings_manager.pg_pool or settings_manager.redis_pool: + log.info("Closing existing database pools before reinitializing...") + await settings_manager.close_pools() + + # Initialize new pools + await settings_manager.initialize_pools() + log.info("Database and cache connection pools initialized for API server.") + success = True + except Exception as e: + retry_count += 1 + log.warning(f"Failed to initialize connection pools (attempt {retry_count}/{max_retries}): {e}") + if retry_count < max_retries: + # Wait before retrying with exponential backoff + wait_time = 2 ** retry_count # 2, 4, 8 seconds + log.info(f"Waiting {wait_time} seconds before retrying...") + await asyncio.sleep(wait_time) + else: + log.exception(f"Failed to initialize connection pools after {max_retries} attempts") + log.error("Dashboard endpoints requiring DB/cache will fail.") + + if not success: + log.error("Failed to initialize database pools after all retries.") else: log.error("settings_manager not imported. Dashboard endpoints requiring DB/cache will fail.") @@ -883,9 +908,28 @@ async def dashboard_get_user_guilds(current_user: dict = Depends(get_dashboard_u # 2. Fetch guilds the bot is in from our DB try: - bot_guild_ids = await settings_manager.get_bot_guild_ids() + # Add retry logic for database operations + max_db_retries = 3 + retry_count = 0 + bot_guild_ids = None + + while retry_count < max_db_retries and bot_guild_ids is None: + try: + bot_guild_ids = await settings_manager.get_bot_guild_ids() + if bot_guild_ids is None: + log.warning(f"Dashboard: Failed to fetch bot guild IDs, retry {retry_count+1}/{max_db_retries}") + 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 + if retry_count < max_db_retries: + await asyncio.sleep(1) # Wait before retrying + + # After retries, if still no data, raise exception if bot_guild_ids is None: - log.error("Dashboard: Failed to fetch bot guild IDs from settings_manager.") + log.error("Dashboard: Failed to fetch bot guild IDs from settings_manager after retries.") raise HTTPException(status_code=500, detail="Could not retrieve bot's guild list.") except Exception as e: log.exception("Dashboard: Exception while fetching bot guild IDs from settings_manager.") diff --git a/restart_api.bat b/restart_api.bat new file mode 100644 index 0000000..88cf413 --- /dev/null +++ b/restart_api.bat @@ -0,0 +1,15 @@ +@echo off +echo Restarting API server... + +REM Change to the discordbot directory +cd /d %~dp0 + +REM Check for running API server processes +echo Checking for running API server processes... +tasklist /fi "imagename eq python.exe" /v | findstr "api_server" + +REM Start the API server +echo Starting API server... +start python run_unified_api.py + +echo API server restart complete. diff --git a/restart_api.py b/restart_api.py new file mode 100644 index 0000000..65a4203 --- /dev/null +++ b/restart_api.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +""" +Script to restart the API server with proper environment setup. +This ensures the database connections are properly initialized. +""" +import os +import sys +import subprocess +import time + +def main(): + """Main function to restart the API server""" + print("Restarting API server...") + + # Get the current directory + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # Check if we're in the discordbot directory + if os.path.basename(current_dir) != "discordbot": + print("Error: This script must be run from the discordbot directory.") + sys.exit(1) + + # Find any running API server processes + try: + print("Checking for running API server processes...") + result = subprocess.run( + ["ps", "-ef", "|", "grep", "api_server.py", "|", "grep", "-v", "grep"], + shell=True, + capture_output=True, + text=True + ) + + if result.stdout.strip(): + print("Found running API server processes:") + print(result.stdout) + + # Ask for confirmation before killing + confirm = input("Do you want to kill these processes? (y/n): ") + if confirm.lower() == 'y': + # Kill the processes + subprocess.run( + ["pkill", "-f", "api_server.py"], + shell=True + ) + print("Processes killed.") + time.sleep(2) # Give processes time to terminate + else: + print("Aborted. Please stop the running API server processes manually.") + sys.exit(1) + except Exception as e: + print(f"Error checking for running processes: {e}") + + # Start the API server + print("Starting API server...") + try: + # Use the run_unified_api.py script + subprocess.Popen( + [sys.executable, "run_unified_api.py"], + cwd=current_dir + ) + print("API server started successfully.") + except Exception as e: + print(f"Error starting API server: {e}") + sys.exit(1) + + print("API server restart complete.") + +if __name__ == "__main__": + main() diff --git a/settings_manager.py b/settings_manager.py index a18bf55..137d0f7 100644 --- a/settings_manager.py +++ b/settings_manager.py @@ -31,18 +31,50 @@ async def initialize_pools(): """Initializes the PostgreSQL and Redis connection pools.""" global pg_pool, redis_pool log.info("Initializing database and cache connection pools...") + + # Close existing pools if they exist + if pg_pool: + log.info("Closing existing PostgreSQL pool before reinitializing...") + await pg_pool.close() + pg_pool = None + + if redis_pool: + log.info("Closing existing Redis pool before reinitializing...") + await redis_pool.close() + redis_pool = None + + # Initialize new pools try: - pg_pool = await asyncpg.create_pool(DATABASE_URL, min_size=1, max_size=10) + # Create PostgreSQL pool with more conservative settings + # Increase max_inactive_connection_lifetime to avoid connections being closed too quickly + pg_pool = await asyncpg.create_pool( + DATABASE_URL, + min_size=1, + max_size=10, + max_inactive_connection_lifetime=60.0, # 60 seconds (default is 10 minutes) + command_timeout=30.0 # 30 seconds timeout for commands + ) log.info(f"PostgreSQL pool connected to {POSTGRES_HOST}/{POSTGRES_DB}") + # Create Redis pool redis_pool = redis.from_url(REDIS_URL, decode_responses=True) - await redis_pool.ping() # Test connection + await redis_pool.ping() # Test connection log.info(f"Redis pool connected to {REDIS_HOST}:{REDIS_PORT}") - await initialize_database() # Ensure tables exist + # Initialize database schema + await initialize_database() # Ensure tables exist + + return True # Indicate successful initialization except Exception as e: log.exception(f"Failed to initialize connection pools: {e}") - # Depending on bot structure, might want to raise or exit here + # Clean up any partially initialized resources + if pg_pool: + await pg_pool.close() + pg_pool = None + if redis_pool: + await redis_pool.close() + redis_pool = None + # Raise the exception to be handled by the caller raise async def close_pools(): @@ -587,17 +619,29 @@ async def get_command_permissions(guild_id: int, command_name: str) -> set[int] 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.""" + global pg_pool if not pg_pool: log.error("Pools not initialized, cannot get bot guild IDs.") return None + + # Create a new connection for this specific operation to avoid event loop conflicts try: - async with 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.") - return guild_ids + # Create a temporary connection just for this operation + # This ensures we're using the current event loop + temp_conn = await asyncpg.connect(DATABASE_URL) + try: + records = await temp_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.") + return guild_ids + finally: + # Always close the temporary connection + await temp_conn.close() + except asyncpg.exceptions.PostgresError as e: + log.exception(f"PostgreSQL error fetching bot guild IDs: {e}") + return None except Exception as e: - log.exception("Database error fetching bot guild IDs.") + log.exception(f"Unexpected error fetching bot guild IDs: {e}") return None