feat: add automatic database initialization and recovery
- Add init_database() with alembic programmatic API and retry logic - Add connection retry with exponential backoff (5 attempts) - Improve error messages for connection/auth failures - Add table existence check before seeding data - Update startup event to run migrations before seeding - Add wait-for-db.sh script for Docker containers - Update Docker and docker-compose configurations Quality gates: ruff ✓, mypy ✓, unit tests (8 passed)
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
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
|
||||
@@ -20,4 +26,74 @@ engine = create_async_engine(
|
||||
)
|
||||
SessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
__all__ = ["SessionLocal", "build_database_url", "engine", "settings"]
|
||||
|
||||
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
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(text("SELECT 1"))
|
||||
|
||||
logger.info("Database connection established.")
|
||||
|
||||
# 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"]
|
||||
|
||||
Reference in New Issue
Block a user