fix(alembic,pipeline,resilience): 数据库迁移与异常处理健壮性修复

- alembic env: 改用事务级 advisory lock (pg_advisory_xact_lock),事务结束自动释放
- 迁移脚本: 使用原生 SQL IF NOT EXISTS/IF EXISTS 替代运行时列检查
- pipeline: SQLAlchemy 异常后先回滚事务再写审计,防止 aborted 状态二次报错
- resilience: ProgrammingError 标记为不可恢复,缩减 DB 重试异常范围
- 前端: 端点规则支持拖拽排序
This commit is contained in:
fawney19
2026-03-02 18:02:21 +08:00
parent 5f1c74aca0
commit 3384c6d666
5 changed files with 218 additions and 49 deletions

View File

@@ -51,7 +51,8 @@ if config.config_file_name is not None:
target_metadata = Base.metadata
# PostgreSQL 全局迁移锁,避免多进程并发执行 Alembic 导致竞态(重复加列/索引等)
# ID 由 crc32("aether-alembic-migration") 拼接生成,仅需全局唯一即可
# 使用事务级 advisory lockpg_advisory_xact_lock在迁移事务结束后自动释放。
# ID 由 crc32("aether-alembic-migration") 拼接生成,仅需全局唯一即可。
MIGRATION_ADVISORY_LOCK_ID = 582694137405821
@@ -89,31 +90,20 @@ def run_migrations_online() -> None:
)
with connectable.connect() as connection:
lock_acquired = False
try:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True, # 比较列类型变更
compare_server_default=True, # 比较默认值变更
)
with context.begin_transaction():
if connection.dialect.name == "postgresql":
connection.execute(
text("SELECT pg_advisory_lock(:lock_id)"),
{"lock_id": MIGRATION_ADVISORY_LOCK_ID},
)
lock_acquired = True
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True, # 比较列类型变更
compare_server_default=True, # 比较默认值变更
)
with context.begin_transaction():
context.run_migrations()
finally:
if lock_acquired:
connection.rollback()
connection.execute(
text("SELECT pg_advisory_unlock(:lock_id)"),
text("SELECT pg_advisory_xact_lock(:lock_id)"),
{"lock_id": MIGRATION_ADVISORY_LOCK_ID},
)
context.run_migrations()
# 根据模式选择运行方式

View File

@@ -9,9 +9,6 @@ from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from sqlalchemy import inspect
from alembic import op
# revision identifiers, used by Alembic.
@@ -21,26 +18,12 @@ branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _column_exists(table_name: str, column_name: str) -> bool:
bind = op.get_bind()
insp = inspect(bind)
columns = [c["name"] for c in insp.get_columns(table_name)]
return column_name in columns
def upgrade() -> None:
if not _column_exists("proxy_nodes", "proxy_metadata"):
op.add_column(
"proxy_nodes",
sa.Column(
"proxy_metadata",
sa.JSON(),
nullable=True,
comment="aether-proxy 上报元数据(版本等)",
),
)
op.execute("ALTER TABLE public.proxy_nodes ADD COLUMN IF NOT EXISTS proxy_metadata json")
op.execute(
"COMMENT ON COLUMN public.proxy_nodes.proxy_metadata IS 'aether-proxy 上报元数据(版本等)'"
)
def downgrade() -> None:
if _column_exists("proxy_nodes", "proxy_metadata"):
op.drop_column("proxy_nodes", "proxy_metadata")
op.execute("ALTER TABLE public.proxy_nodes DROP COLUMN IF EXISTS proxy_metadata")