from __future__ import annotations import asyncio import sys from sqlalchemy import text from sqlalchemy.ext.asyncio import create_async_engine from .config import Config ATTEMPTS = 60 DELAY_SECONDS = 1.0 async def _probe(url: str) -> None: engine = create_async_engine(url) try: async with engine.connect() as conn: await conn.execute(text("SELECT 1")) finally: await engine.dispose() async def wait_for_db(attempts: int = ATTEMPTS, delay: float = DELAY_SECONDS) -> bool: """Poll the database until it accepts a connection, up to `attempts` tries. Startup runs `alembic upgrade head` as the container's first DB touch, so a database that is a moment slow to accept connections would otherwise crash the container. This gives it a short window to come up, verifying once per `delay` seconds, instead of failing on the first missed connection. """ url = Config.DATABASE_URL for attempt in range(1, attempts + 1): try: await _probe(url) print(f"[dbwait] database ready (attempt {attempt}/{attempts})", flush=True) return True except Exception as exc: print( f"[dbwait] not ready (attempt {attempt}/{attempts}): {exc.__class__.__name__}: {exc}", flush=True, ) if attempt < attempts: await asyncio.sleep(delay) print(f"[dbwait] database unreachable after {attempts} attempts; giving up", flush=True) return False def main() -> int: return 0 if asyncio.run(wait_for_db()) else 1 if __name__ == "__main__": sys.exit(main())