Files
headquarter/apps/api/src/database.py
T
Fusion 285d3dace8 fix: use await engine.connect() instead of async with
SQLAlchemy 2.0 async engine.connect() doesn't support context manager.
Use explicit connect/close instead.
2026-05-18 22:38:08 +02:00

124 lines
4.3 KiB
Python

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"]