285d3dace8
SQLAlchemy 2.0 async engine.connect() doesn't support context manager. Use explicit connect/close instead.
124 lines
4.3 KiB
Python
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"]
|