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:
Fusion
2026-05-18 22:09:52 +02:00
parent 29c3563148
commit ea6c466c6c
11 changed files with 338 additions and 22 deletions
+77 -1
View File
@@ -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"]