fix: use subprocess for alembic migrations to avoid async/sync issues
SQLAlchemy 2.0 async engines conflict with alembic's sync context manager. Instead of trying to bridge async/sync, use subprocess to run 'alembic upgrade head' directly. This is simpler and more reliable. - Remove psycopg2-binary dependency (no longer needed) - Simplify init_database to use subprocess.run() - Remove all sync engine code
This commit is contained in:
@@ -8,7 +8,6 @@ dependencies = [
|
|||||||
"uvicorn[standard]>=0.24.0",
|
"uvicorn[standard]>=0.24.0",
|
||||||
"sqlalchemy>=2.0.0",
|
"sqlalchemy>=2.0.0",
|
||||||
"asyncpg>=0.29.0",
|
"asyncpg>=0.29.0",
|
||||||
"psycopg2-binary>=2.9.0",
|
|
||||||
"alembic>=1.12.0",
|
"alembic>=1.12.0",
|
||||||
"pydantic>=2.5.0",
|
"pydantic>=2.5.0",
|
||||||
"pydantic-settings>=2.1.0",
|
"pydantic-settings>=2.1.0",
|
||||||
|
|||||||
+27
-46
@@ -1,10 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from alembic import command
|
|
||||||
from alembic.config import Config
|
|
||||||
from alembic.runtime.migration import MigrationContext
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
|
|
||||||
@@ -29,30 +26,20 @@ engine = create_async_engine(
|
|||||||
SessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
SessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
|
|
||||||
|
|
||||||
def _get_sync_database_url() -> str:
|
|
||||||
"""Convert async database URL to sync URL for alembic."""
|
|
||||||
sync_url = database_url
|
|
||||||
# Replace asyncpg with psycopg2 for sync operations
|
|
||||||
if "+asyncpg" in sync_url:
|
|
||||||
sync_url = sync_url.replace("+asyncpg", "+psycopg2")
|
|
||||||
return sync_url
|
|
||||||
|
|
||||||
|
|
||||||
async def init_database(
|
async def init_database(
|
||||||
max_retries: int = 5,
|
max_retries: int = 5,
|
||||||
retry_delay: float = 2.0,
|
retry_delay: float = 2.0,
|
||||||
alembic_ini_path: str = "alembic.ini",
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Initialize the database by running pending migrations.
|
"""Initialize the database by running pending migrations.
|
||||||
|
|
||||||
Retries the connection with exponential backoff to handle
|
Uses subprocess to run 'alembic upgrade head' to avoid
|
||||||
database startup latency in containerized environments.
|
async/sync context manager issues with SQLAlchemy 2.0.
|
||||||
|
|
||||||
Returns True if migrations succeeded, False otherwise.
|
Returns True if migrations succeeded, False otherwise.
|
||||||
"""
|
"""
|
||||||
for attempt in range(1, max_retries + 1):
|
for attempt in range(1, max_retries + 1):
|
||||||
try:
|
try:
|
||||||
# Test basic connectivity (async)
|
# Test basic connectivity
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
test_conn = await engine.connect()
|
test_conn = await engine.connect()
|
||||||
try:
|
try:
|
||||||
@@ -62,36 +49,30 @@ async def init_database(
|
|||||||
|
|
||||||
logger.info("Database connection established.")
|
logger.info("Database connection established.")
|
||||||
|
|
||||||
# Use sync engine for alembic operations
|
# Run migrations via subprocess
|
||||||
sync_url = _get_sync_database_url()
|
logger.info("Running database migrations...")
|
||||||
sync_engine = create_engine(sync_url, poolclass=NullPool)
|
result = await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: subprocess.run(
|
||||||
|
["alembic", "upgrade", "head"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd="/app",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
if result.returncode == 0:
|
||||||
# Check if migrations are already complete
|
logger.info("Database migrations completed successfully.")
|
||||||
with sync_engine.connect() as sync_conn:
|
logger.debug("Alembic output: %s", result.stdout)
|
||||||
context = MigrationContext.configure(sync_conn)
|
return True
|
||||||
current_rev = context.get_current_revision()
|
else:
|
||||||
|
logger.error("Migration failed: %s", result.stderr)
|
||||||
alembic_cfg = Config(alembic_ini_path)
|
if attempt < max_retries:
|
||||||
script = command.ScriptDirectory.from_config(alembic_cfg)
|
wait = retry_delay * (2 ** (attempt - 1))
|
||||||
head_rev = script.get_current_head()
|
logger.info("Retrying in %.1f seconds...", wait)
|
||||||
|
await asyncio.sleep(wait)
|
||||||
if current_rev == head_rev:
|
else:
|
||||||
logger.info("Database is already at the latest migration (%s).", head_rev)
|
return False
|
||||||
return True
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Current revision: %s, Head revision: %s. Running migrations...",
|
|
||||||
current_rev,
|
|
||||||
head_rev,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Run alembic migrations
|
|
||||||
command.upgrade(alembic_cfg, "head")
|
|
||||||
logger.info("Database migrations completed successfully.")
|
|
||||||
return True
|
|
||||||
finally:
|
|
||||||
sync_engine.dispose()
|
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
error_msg = str(exc).lower()
|
error_msg = str(exc).lower()
|
||||||
|
|||||||
Reference in New Issue
Block a user