Files
life-echo/api/app/core/alembic_startup.py

101 lines
3.1 KiB
Python
Raw Normal View History

"""
启动时执行 Alembic 迁移 CLI `uv run alembic upgrade head` 等价
- 对瞬时连接错误重试数据库尚未就绪网络抖动
- 可通过 settings 关闭迁移或失败时中止进程生产推荐
"""
from __future__ import annotations
import time
from pathlib import Path
from typing import Final
from sqlalchemy.exc import OperationalError
from app.core.config import settings
from app.core.logging import get_logger
logger = get_logger(__name__)
_API_DIR: Final[Path] = Path(__file__).resolve().parent.parent.parent
def _run_alembic_upgrade_once() -> None:
from alembic.command import upgrade
from alembic.config import Config
cfg = Config(str(_API_DIR / "alembic.ini"))
upgrade(cfg, "head")
def _is_retryable_migration_error(err: BaseException) -> bool:
if isinstance(err, OperationalError):
return True
msg = str(err).lower()
needles = (
"connection refused",
"could not connect",
"connection reset",
"timeout",
"server closed the connection",
"the database system is starting",
"database system is shutting down",
"too many connections",
)
return any(n in msg for n in needles)
def run_alembic_upgrade_at_startup() -> None:
"""
同步执行迁移失败时按 settings 记录日志或抛出
asyncio 中请使用 ``asyncio.to_thread(run_alembic_upgrade_at_startup)``
避免阻塞事件循环
"""
if not settings.alembic_run_on_startup:
logger.info("跳过 Alembic 迁移alembic_run_on_startup=False")
return
max_tries = max(1, settings.alembic_startup_max_retries)
base_delay = float(settings.alembic_startup_retry_base_seconds)
last: BaseException | None = None
for attempt in range(max_tries):
try:
_run_alembic_upgrade_once()
if attempt > 0:
logger.info(
"Alembic 迁移成功(第 {} 次尝试)",
attempt + 1,
)
else:
logger.info("Alembic 迁移已就绪")
return
except Exception as e:
last = e
will_retry = attempt < max_tries - 1 and _is_retryable_migration_error(e)
if will_retry:
delay = base_delay * (2**attempt)
logger.warning(
"Alembic 迁移失败(将重试): {}{:.1f}s 后重试 ({}/{})",
e,
delay,
attempt + 1,
max_tries,
)
time.sleep(delay)
else:
break
assert last is not None
logger.exception("Alembic 迁移失败: {}", last)
if settings.alembic_startup_fail_fast:
raise RuntimeError(
"Alembic migration failed; set ALEMBIC_STARTUP_FAIL_FAST=false "
"for dev-only tolerate mode"
) from last
logger.error(
"应用将继续启动,但数据库结构可能未更新;请检查 DATABASE_URL 与 PostgreSQL 是否可达"
)