mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
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:
@@ -51,7 +51,8 @@ if config.config_file_name is not None:
|
|||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
# PostgreSQL 全局迁移锁,避免多进程并发执行 Alembic 导致竞态(重复加列/索引等)
|
# PostgreSQL 全局迁移锁,避免多进程并发执行 Alembic 导致竞态(重复加列/索引等)
|
||||||
# ID 由 crc32("aether-alembic-migration") 拼接生成,仅需全局唯一即可
|
# 使用事务级 advisory lock(pg_advisory_xact_lock),在迁移事务结束后自动释放。
|
||||||
|
# ID 由 crc32("aether-alembic-migration") 拼接生成,仅需全局唯一即可。
|
||||||
MIGRATION_ADVISORY_LOCK_ID = 582694137405821
|
MIGRATION_ADVISORY_LOCK_ID = 582694137405821
|
||||||
|
|
||||||
|
|
||||||
@@ -89,31 +90,20 @@ def run_migrations_online() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
with connectable.connect() as connection:
|
||||||
lock_acquired = False
|
context.configure(
|
||||||
try:
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
compare_type=True, # 比较列类型变更
|
||||||
|
compare_server_default=True, # 比较默认值变更
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
if connection.dialect.name == "postgresql":
|
if connection.dialect.name == "postgresql":
|
||||||
connection.execute(
|
connection.execute(
|
||||||
text("SELECT pg_advisory_lock(:lock_id)"),
|
text("SELECT pg_advisory_xact_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)"),
|
|
||||||
{"lock_id": MIGRATION_ADVISORY_LOCK_ID},
|
{"lock_id": MIGRATION_ADVISORY_LOCK_ID},
|
||||||
)
|
)
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
# 根据模式选择运行方式
|
# 根据模式选择运行方式
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy import inspect
|
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -21,26 +18,12 @@ branch_labels: str | Sequence[str] | None = None
|
|||||||
depends_on: 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:
|
def upgrade() -> None:
|
||||||
if not _column_exists("proxy_nodes", "proxy_metadata"):
|
op.execute("ALTER TABLE public.proxy_nodes ADD COLUMN IF NOT EXISTS proxy_metadata json")
|
||||||
op.add_column(
|
op.execute(
|
||||||
"proxy_nodes",
|
"COMMENT ON COLUMN public.proxy_nodes.proxy_metadata IS 'aether-proxy 上报元数据(版本等)'"
|
||||||
sa.Column(
|
)
|
||||||
"proxy_metadata",
|
|
||||||
sa.JSON(),
|
|
||||||
nullable=True,
|
|
||||||
comment="aether-proxy 上报元数据(版本等)",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
if _column_exists("proxy_nodes", "proxy_metadata"):
|
op.execute("ALTER TABLE public.proxy_nodes DROP COLUMN IF EXISTS proxy_metadata")
|
||||||
op.drop_column("proxy_nodes", "proxy_metadata")
|
|
||||||
|
|||||||
@@ -208,12 +208,36 @@
|
|||||||
</div>
|
</div>
|
||||||
<CollapsibleContent class="pt-3">
|
<CollapsibleContent class="pt-3">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-if="getEndpointRulesCount(endpoint) > 1 || getEndpointBodyRulesCount(endpoint) > 1"
|
||||||
|
class="flex items-center gap-1.5 text-xs text-muted-foreground px-2"
|
||||||
|
>
|
||||||
|
<GripVertical class="w-3.5 h-3.5" />
|
||||||
|
<span>拖拽左侧手柄可调整规则执行顺序</span>
|
||||||
|
</div>
|
||||||
<!-- 请求头规则列表 - 主题色边框 -->
|
<!-- 请求头规则列表 - 主题色边框 -->
|
||||||
<div
|
<div
|
||||||
v-for="(rule, index) in getEndpointEditRules(endpoint.id)"
|
v-for="(rule, index) in getEndpointEditRules(endpoint.id)"
|
||||||
:key="`header-${index}`"
|
:key="`header-${index}`"
|
||||||
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md border-l-4 border-primary/60 bg-muted/30"
|
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md border-l-4 border-primary/60 bg-muted/30"
|
||||||
|
:class="[
|
||||||
|
isHeaderRuleDragging(endpoint.id, index) ? 'opacity-60 border-primary bg-primary/5' : '',
|
||||||
|
isHeaderRuleDragOver(endpoint.id, index) ? 'ring-1 ring-primary/40 bg-primary/10' : ''
|
||||||
|
]"
|
||||||
|
@dragover.prevent="handleHeaderRuleDragOver(endpoint.id, index)"
|
||||||
|
@dragleave="handleHeaderRuleDragLeave(endpoint.id, index)"
|
||||||
|
@drop.prevent="handleHeaderRuleDrop(endpoint.id, index)"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h-7 w-6 shrink-0 inline-flex items-center justify-center rounded-sm text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted cursor-grab active:cursor-grabbing"
|
||||||
|
title="拖拽排序"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="(e) => handleHeaderRuleDragStart(endpoint.id, index, e)"
|
||||||
|
@dragend="() => handleHeaderRuleDragEnd(endpoint.id)"
|
||||||
|
>
|
||||||
|
<GripVertical class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
<span
|
<span
|
||||||
class="text-[10px] font-semibold text-primary shrink-0"
|
class="text-[10px] font-semibold text-primary shrink-0"
|
||||||
title="请求头"
|
title="请求头"
|
||||||
@@ -382,7 +406,24 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md border-l-4 border-muted-foreground/40 bg-muted/30"
|
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md border-l-4 border-muted-foreground/40 bg-muted/30"
|
||||||
|
:class="[
|
||||||
|
isBodyRuleDragging(endpoint.id, index) ? 'opacity-60 border-muted-foreground/70 bg-muted/50' : '',
|
||||||
|
isBodyRuleDragOver(endpoint.id, index) ? 'ring-1 ring-muted-foreground/40 bg-muted/40' : ''
|
||||||
|
]"
|
||||||
|
@dragover.prevent="handleBodyRuleDragOver(endpoint.id, index)"
|
||||||
|
@dragleave="handleBodyRuleDragLeave(endpoint.id, index)"
|
||||||
|
@drop.prevent="handleBodyRuleDrop(endpoint.id, index)"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h-7 w-6 shrink-0 inline-flex items-center justify-center rounded-sm text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted cursor-grab active:cursor-grabbing"
|
||||||
|
title="拖拽排序"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="(e) => handleBodyRuleDragStart(endpoint.id, index, e)"
|
||||||
|
@dragend="() => handleBodyRuleDragEnd(endpoint.id)"
|
||||||
|
>
|
||||||
|
<GripVertical class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
<span
|
<span
|
||||||
class="text-[10px] font-semibold text-muted-foreground shrink-0"
|
class="text-[10px] font-semibold text-muted-foreground shrink-0"
|
||||||
title="请求体"
|
title="请求体"
|
||||||
@@ -781,7 +822,7 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
} from '@/components/ui'
|
} from '@/components/ui'
|
||||||
import { Settings, Trash2, Check, X, Power, ChevronRight, Plus, Shuffle, RotateCcw, Radio, CheckCircle, Save, Filter, HelpCircle } from 'lucide-vue-next'
|
import { Settings, Trash2, Check, X, Power, ChevronRight, Plus, Shuffle, RotateCcw, Radio, CheckCircle, Save, Filter, HelpCircle, GripVertical } from 'lucide-vue-next'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { parseApiError } from '@/utils/errorParser'
|
import { parseApiError } from '@/utils/errorParser'
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
@@ -916,6 +957,130 @@ function handleBodyRuleSelectOpen(endpointId: string, index: number, open: boole
|
|||||||
bodyRuleSelectOpen.value[`${endpointId}-${index}`] = open
|
bodyRuleSelectOpen.value[`${endpointId}-${index}`] = open
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearHeaderRuleSelectOpen(endpointId: string) {
|
||||||
|
Object.keys(ruleSelectOpen.value).forEach((key) => {
|
||||||
|
if (key.startsWith(`${endpointId}-`)) {
|
||||||
|
delete ruleSelectOpen.value[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBodyRuleSelectOpen(endpointId: string) {
|
||||||
|
Object.keys(bodyRuleSelectOpen.value).forEach((key) => {
|
||||||
|
if (key.startsWith(`${endpointId}-`)) {
|
||||||
|
delete bodyRuleSelectOpen.value[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHeaderRuleDragging(endpointId: string, index: number): boolean {
|
||||||
|
return headerRuleDraggedIndex.value[endpointId] === index
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHeaderRuleDragOver(endpointId: string, index: number): boolean {
|
||||||
|
return headerRuleDragOverIndex.value[endpointId] === index
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBodyRuleDragging(endpointId: string, index: number): boolean {
|
||||||
|
return bodyRuleDraggedIndex.value[endpointId] === index
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBodyRuleDragOver(endpointId: string, index: number): boolean {
|
||||||
|
return bodyRuleDragOverIndex.value[endpointId] === index
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHeaderRuleDragState(endpointId: string) {
|
||||||
|
headerRuleDraggedIndex.value[endpointId] = null
|
||||||
|
headerRuleDragOverIndex.value[endpointId] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBodyRuleDragState(endpointId: string) {
|
||||||
|
bodyRuleDraggedIndex.value[endpointId] = null
|
||||||
|
bodyRuleDragOverIndex.value[endpointId] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHeaderRuleDragStart(endpointId: string, index: number, event: DragEvent) {
|
||||||
|
const rules = getEndpointEditRules(endpointId)
|
||||||
|
if (!rules[index]) return
|
||||||
|
|
||||||
|
headerRuleDraggedIndex.value[endpointId] = index
|
||||||
|
headerRuleDragOverIndex.value[endpointId] = null
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
|
event.dataTransfer.setData('text/plain', `header:${endpointId}:${index}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHeaderRuleDragOver(endpointId: string, index: number) {
|
||||||
|
const dragged = headerRuleDraggedIndex.value[endpointId]
|
||||||
|
if (dragged === null || dragged === undefined || dragged === index) return
|
||||||
|
headerRuleDragOverIndex.value[endpointId] = index
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHeaderRuleDragLeave(endpointId: string, index: number) {
|
||||||
|
if (headerRuleDragOverIndex.value[endpointId] === index) {
|
||||||
|
headerRuleDragOverIndex.value[endpointId] = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHeaderRuleDrop(endpointId: string, targetIndex: number) {
|
||||||
|
const dragIndex = headerRuleDraggedIndex.value[endpointId]
|
||||||
|
clearHeaderRuleDragState(endpointId)
|
||||||
|
if (dragIndex === null || dragIndex === undefined || dragIndex === targetIndex) return
|
||||||
|
|
||||||
|
const rules = getEndpointEditRules(endpointId)
|
||||||
|
if (dragIndex < 0 || dragIndex >= rules.length || targetIndex < 0 || targetIndex >= rules.length) return
|
||||||
|
|
||||||
|
const [draggedRule] = rules.splice(dragIndex, 1)
|
||||||
|
rules.splice(targetIndex, 0, draggedRule)
|
||||||
|
clearHeaderRuleSelectOpen(endpointId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHeaderRuleDragEnd(endpointId: string) {
|
||||||
|
clearHeaderRuleDragState(endpointId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBodyRuleDragStart(endpointId: string, index: number, event: DragEvent) {
|
||||||
|
const rules = getEndpointEditBodyRules(endpointId)
|
||||||
|
if (!rules[index]) return
|
||||||
|
|
||||||
|
bodyRuleDraggedIndex.value[endpointId] = index
|
||||||
|
bodyRuleDragOverIndex.value[endpointId] = null
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
|
event.dataTransfer.setData('text/plain', `body:${endpointId}:${index}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBodyRuleDragOver(endpointId: string, index: number) {
|
||||||
|
const dragged = bodyRuleDraggedIndex.value[endpointId]
|
||||||
|
if (dragged === null || dragged === undefined || dragged === index) return
|
||||||
|
bodyRuleDragOverIndex.value[endpointId] = index
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBodyRuleDragLeave(endpointId: string, index: number) {
|
||||||
|
if (bodyRuleDragOverIndex.value[endpointId] === index) {
|
||||||
|
bodyRuleDragOverIndex.value[endpointId] = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBodyRuleDrop(endpointId: string, targetIndex: number) {
|
||||||
|
const dragIndex = bodyRuleDraggedIndex.value[endpointId]
|
||||||
|
clearBodyRuleDragState(endpointId)
|
||||||
|
if (dragIndex === null || dragIndex === undefined || dragIndex === targetIndex) return
|
||||||
|
|
||||||
|
const rules = getEndpointEditBodyRules(endpointId)
|
||||||
|
if (dragIndex < 0 || dragIndex >= rules.length || targetIndex < 0 || targetIndex >= rules.length) return
|
||||||
|
|
||||||
|
const [draggedRule] = rules.splice(dragIndex, 1)
|
||||||
|
rules.splice(targetIndex, 0, draggedRule)
|
||||||
|
clearBodyRuleSelectOpen(endpointId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBodyRuleDragEnd(endpointId: string) {
|
||||||
|
clearBodyRuleDragState(endpointId)
|
||||||
|
}
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const addingEndpoint = ref(false)
|
const addingEndpoint = ref(false)
|
||||||
const savingEndpointId = ref<string | null>(null)
|
const savingEndpointId = ref<string | null>(null)
|
||||||
@@ -937,6 +1102,12 @@ const bodyRuleSelectOpen = ref<Record<string, boolean>>({})
|
|||||||
// 请求体规则说明 Popover 的展开状态
|
// 请求体规则说明 Popover 的展开状态
|
||||||
const bodyRuleHelpOpenEndpointId = ref<string | null>(null)
|
const bodyRuleHelpOpenEndpointId = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 规则拖拽状态(按 endpoint 维度)
|
||||||
|
const headerRuleDraggedIndex = ref<Record<string, number | null>>({})
|
||||||
|
const headerRuleDragOverIndex = ref<Record<string, number | null>>({})
|
||||||
|
const bodyRuleDraggedIndex = ref<Record<string, number | null>>({})
|
||||||
|
const bodyRuleDragOverIndex = ref<Record<string, number | null>>({})
|
||||||
|
|
||||||
function setBodyRuleHelpOpen(endpointId: string, open: boolean) {
|
function setBodyRuleHelpOpen(endpointId: string, open: boolean) {
|
||||||
bodyRuleHelpOpenEndpointId.value = open ? endpointId : null
|
bodyRuleHelpOpenEndpointId.value = open ? endpointId : null
|
||||||
}
|
}
|
||||||
@@ -1230,6 +1401,8 @@ function handleAddEndpointRule(endpointId: string) {
|
|||||||
function removeEndpointRule(endpointId: string, index: number) {
|
function removeEndpointRule(endpointId: string, index: number) {
|
||||||
const rules = getEndpointEditRules(endpointId)
|
const rules = getEndpointEditRules(endpointId)
|
||||||
rules.splice(index, 1)
|
rules.splice(index, 1)
|
||||||
|
clearHeaderRuleDragState(endpointId)
|
||||||
|
clearHeaderRuleSelectOpen(endpointId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新规则类型
|
// 更新规则类型
|
||||||
@@ -1368,6 +1541,8 @@ function handleAddEndpointBodyRule(endpointId: string) {
|
|||||||
function removeEndpointBodyRule(endpointId: string, index: number) {
|
function removeEndpointBodyRule(endpointId: string, index: number) {
|
||||||
const rules = getEndpointEditBodyRules(endpointId)
|
const rules = getEndpointEditBodyRules(endpointId)
|
||||||
rules.splice(index, 1)
|
rules.splice(index, 1)
|
||||||
|
clearBodyRuleDragState(endpointId)
|
||||||
|
clearBodyRuleSelectOpen(endpointId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新请求体规则类型
|
// 更新请求体规则类型
|
||||||
@@ -1933,6 +2108,12 @@ onMounted(() => {
|
|||||||
// 监听 props 变化
|
// 监听 props 变化
|
||||||
watch(() => props.modelValue, (open) => {
|
watch(() => props.modelValue, (open) => {
|
||||||
bodyRuleHelpOpenEndpointId.value = null
|
bodyRuleHelpOpenEndpointId.value = null
|
||||||
|
ruleSelectOpen.value = {}
|
||||||
|
bodyRuleSelectOpen.value = {}
|
||||||
|
headerRuleDraggedIndex.value = {}
|
||||||
|
headerRuleDragOverIndex.value = {}
|
||||||
|
bodyRuleDraggedIndex.value = {}
|
||||||
|
bodyRuleDragOverIndex.value = {}
|
||||||
if (open) {
|
if (open) {
|
||||||
localEndpoints.value = [...(props.endpoints || [])]
|
localEndpoints.value = [...(props.endpoints || [])]
|
||||||
// 清空编辑状态,重新从端点加载
|
// 清空编辑状态,重新从端点加载
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from enum import Enum
|
|||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from src.config.settings import config
|
from src.config.settings import config
|
||||||
@@ -199,6 +200,12 @@ class ApiRequestPipeline:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
handle_duration = PerfRecorder.stop(handle_start, "pipeline_handle", labels=perf_labels)
|
handle_duration = PerfRecorder.stop(handle_start, "pipeline_handle", labels=perf_labels)
|
||||||
_record_perf_metric("handle_ms", handle_duration)
|
_record_perf_metric("handle_ms", handle_duration)
|
||||||
|
if isinstance(exc, SQLAlchemyError):
|
||||||
|
# SQL 执行失败后事务会进入 aborted 状态;先回滚,避免审计写入二次报错。
|
||||||
|
try:
|
||||||
|
context.db.rollback()
|
||||||
|
except Exception as rollback_exc:
|
||||||
|
logger.debug(f"[Pipeline] 回滚失败(可忽略): {rollback_exc}")
|
||||||
self._record_audit_event(
|
self._record_audit_event(
|
||||||
context,
|
context,
|
||||||
adapter,
|
adapter,
|
||||||
|
|||||||
@@ -128,19 +128,27 @@ class ResilienceManager:
|
|||||||
# 数据库连接错误 - 只捕获特定的数据库相关异常
|
# 数据库连接错误 - 只捕获特定的数据库相关异常
|
||||||
try:
|
try:
|
||||||
from sqlalchemy.exc import (
|
from sqlalchemy.exc import (
|
||||||
DatabaseError,
|
|
||||||
DisconnectionError,
|
DisconnectionError,
|
||||||
OperationalError,
|
OperationalError,
|
||||||
StatementError,
|
ProgrammingError,
|
||||||
)
|
)
|
||||||
from sqlalchemy.exc import TimeoutError as SQLTimeoutError
|
from sqlalchemy.exc import TimeoutError as SQLTimeoutError
|
||||||
|
|
||||||
|
# SQL/Schema 编程错误(如缺列/缺表)不应误判为“连接异常重试”。
|
||||||
|
self.add_error_pattern(
|
||||||
|
ErrorPattern(
|
||||||
|
error_types=[ProgrammingError],
|
||||||
|
severity=ErrorSeverity.HIGH,
|
||||||
|
recovery_strategy=RecoveryStrategy.USER_NOTIFY,
|
||||||
|
user_message="数据库结构与当前版本不兼容,请执行 alembic upgrade head 后重试",
|
||||||
|
auto_recover=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
db_exceptions = [
|
db_exceptions = [
|
||||||
OperationalError,
|
OperationalError,
|
||||||
DisconnectionError,
|
DisconnectionError,
|
||||||
SQLTimeoutError,
|
SQLTimeoutError,
|
||||||
StatementError,
|
|
||||||
DatabaseError,
|
|
||||||
]
|
]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# 如果SQLAlchemy不可用,使用通用异常类型
|
# 如果SQLAlchemy不可用,使用通用异常类型
|
||||||
|
|||||||
Reference in New Issue
Block a user