fix(init_db): 启动迁移失败不再静默——logger.exception 带 traceback + schema 覆盖校验
2026-09-26 事故复盘:AUTO_MIGRATE=true 但启动迁移在 stamp 之前就抛异常,
except 里 logger.error(f"{e}") 丢掉了 traceback,docker logs 只剩一句
"数据库初始化失败",prod 缺 stp_files.product_id 等列导致上传全挂,
无从定位。两个修复:
- 初始化异常改用 logger.exception,完整 traceback 落日志
- 新增启动期 _verify_schema_coverage:模型声明的 32 张表全部列必须真实
存在于 DB,缺失即 error 点名(只查缺不查多,遗留列不报);迁移后自动
执行,只读、自身异常不影响启动
已对生产库实跑验证:32 张表全部通过。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -184,6 +184,51 @@ async def create_admin_user(session):
|
|||||||
logger.info(f"创建了管理员账户: {settings.ADMIN_USERNAME}")
|
logger.info(f"创建了管理员账户: {settings.ADMIN_USERNAME}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _verify_schema_coverage() -> None:
|
||||||
|
"""启动期 schema 校验:模型声明的表/列必须真实存在于 DB。
|
||||||
|
|
||||||
|
背景(2026-09-26 事故):AUTO_MIGRATE 启动迁移曾静默失败,prod 库缺
|
||||||
|
stp_files.product_id 等列,上传全挂两天无启动日志线索。此校验保证
|
||||||
|
『迁移被跳过 / stamp 基线掩盖未应用增量』这类漂移在启动时即被点名。
|
||||||
|
|
||||||
|
只查缺、不查多:DB 里的遗留列/表(如 users.is_superuser)是历史产物,不管。
|
||||||
|
发现缺失只记 error 不抛——服务照常起,但 docker logs 必有醒目线索。
|
||||||
|
"""
|
||||||
|
import moldinsight.models # noqa: F401
|
||||||
|
import inventory.models # noqa: F401
|
||||||
|
from shared.models.base import Base
|
||||||
|
|
||||||
|
async with db_manager.engine.connect() as conn:
|
||||||
|
rows = (await conn.execute(text(
|
||||||
|
"SELECT table_name, column_name FROM information_schema.columns "
|
||||||
|
"WHERE table_schema='public'"
|
||||||
|
))).all()
|
||||||
|
db_cols: dict = {}
|
||||||
|
for tn, cn in rows:
|
||||||
|
db_cols.setdefault(tn, set()).add(cn)
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
for tname, table in Base.metadata.tables.items():
|
||||||
|
present = db_cols.get(tname)
|
||||||
|
if present is None:
|
||||||
|
missing.append(f"整表缺失: {tname}")
|
||||||
|
continue
|
||||||
|
absent = {c.name for c in table.columns} - present
|
||||||
|
if absent:
|
||||||
|
missing.append(f"{tname} 缺列: {sorted(absent)}")
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
logger.error(
|
||||||
|
"schema 校验失败:模型声明了 %d 处 DB 缺失(迁移链与实际 schema 不一致,"
|
||||||
|
"请人工执行 alembic upgrade head 或核对基线):%s",
|
||||||
|
len(missing), "; ".join(missing),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"schema 校验通过:模型 %d 张表的全部列均存在于 DB", len(Base.metadata.tables)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def init_database(keep_connected: bool = True):
|
async def init_database(keep_connected: bool = True):
|
||||||
"""初始化数据库"""
|
"""初始化数据库"""
|
||||||
try:
|
try:
|
||||||
@@ -196,6 +241,12 @@ async def init_database(keep_connected: bool = True):
|
|||||||
"schema 由部署流程单点执行(alembic CLI 或 python -m shared.database.init_db)"
|
"schema 由部署流程单点执行(alembic CLI 或 python -m shared.database.init_db)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 迁移后校验(只读、不抛):迁移链与实际 schema 脱节时在启动日志直接点名
|
||||||
|
try:
|
||||||
|
await _verify_schema_coverage()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("schema 校验自身异常(不影响启动流程)")
|
||||||
|
|
||||||
async with db_manager.session() as session:
|
async with db_manager.session() as session:
|
||||||
perm_map = await init_permissions(session)
|
perm_map = await init_permissions(session)
|
||||||
if perm_map is None:
|
if perm_map is None:
|
||||||
@@ -223,7 +274,9 @@ async def init_database(keep_connected: bool = True):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"数据库初始化失败: {e}")
|
# logger.exception 而非 error(f"{e}"):吞掉 traceback 曾让启动迁移静默
|
||||||
|
# 失败两天无从排查(2026-09-26 事故)
|
||||||
|
logger.exception(f"数据库初始化失败: {e}")
|
||||||
print(f"数据库初始化失败: {e}")
|
print(f"数据库初始化失败: {e}")
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
Reference in New Issue
Block a user