import asyncio import logging from alembic import command from alembic.config import Config from alembic.runtime.migration import MigrationContext from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool from src.config import Settings, build_database_url logger = logging.getLogger(__name__) settings = Settings() database_url = settings.database_url # SQLite requires aiosqlite and different connect args connect_args = {} if database_url.startswith("sqlite"): connect_args = {"check_same_thread": False} engine = create_async_engine( database_url, future=True, poolclass=NullPool, connect_args=connect_args, ) SessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async def init_database( max_retries: int = 5, retry_delay: float = 2.0, alembic_ini_path: str = "alembic.ini", ) -> bool: """Initialize the database by running pending migrations. Retries the connection with exponential backoff to handle database startup latency in containerized environments. Returns True if migrations succeeded, False otherwise. """ for attempt in range(1, max_retries + 1): try: # Test basic connectivity from sqlalchemy import text conn = await engine.connect() try: await conn.execute(text("SELECT 1")) finally: await conn.close() logger.info("Database connection established.") # Check if migrations are already complete def _check_migrations(): alembic_cfg = Config(alembic_ini_path) script = command.ScriptDirectory.from_config(alembic_cfg) with engine.connect() as conn: context = MigrationContext.configure(conn) current_rev = context.get_current_revision() head_rev = script.get_current_head() return current_rev == head_rev, current_rev, head_rev is_current, current_rev, head_rev = await asyncio.get_event_loop().run_in_executor( None, _check_migrations ) if is_current: logger.info("Database is already at the latest migration (%s).", head_rev) return True logger.info("Current revision: %s, Head revision: %s. Running migrations...", current_rev, head_rev) # Run alembic migrations (sync call in executor) def _run_migrations(): alembic_cfg = Config(alembic_ini_path) command.upgrade(alembic_cfg, "head") await asyncio.get_event_loop().run_in_executor(None, _run_migrations) logger.info("Database migrations completed successfully.") return True except Exception as exc: error_msg = str(exc).lower() if "connection" in error_msg or "could not connect" in error_msg: logger.warning( "Database connection failed (attempt %d/%d): %s", attempt, max_retries, exc, ) elif "authentication" in error_msg or "password" in error_msg: logger.error( "Database authentication failed: %s. " "Check POSTGRES_USER and POSTGRES_PASSWORD environment variables.", exc, ) return False else: logger.error( "Database initialization error (attempt %d/%d): %s", attempt, max_retries, exc, ) if attempt < max_retries: wait = retry_delay * (2 ** (attempt - 1)) logger.info("Retrying in %.1f seconds...", wait) await asyncio.sleep(wait) else: logger.error( "Failed to initialize database after %d attempts. " "Ensure the database is running and accessible.", max_retries, ) return False return False __all__ = ["SessionLocal", "build_database_url", "engine", "settings", "init_database"]