mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 03:48:03 +08:00
80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
"""
|
||
数据库错误诊断。
|
||
|
||
把驱动层的错误分类字段(sqlite3 / psycopg2 / asyncpg 各不相同)提取成统一结构,
|
||
并挂到引擎的异常事件上;SQLite I/O 故障还会使坏连接失效,避免连接池持续复用。
|
||
"""
|
||
import sqlite3
|
||
from typing import Any, Optional
|
||
|
||
from sqlalchemy import event
|
||
from sqlalchemy.engine import Engine as SQLAlchemyEngine
|
||
from sqlalchemy.engine import ExceptionContext
|
||
|
||
from app.runtime.log import logger
|
||
|
||
# SQLite 扩展错误码的低 8 位是主错误码,覆盖短读、读失败和写失败等 I/O 子类。
|
||
_SQLITE_PRIMARY_ERROR_MASK = 0xFF
|
||
|
||
|
||
def _database_error_metadata(error: BaseException) -> Optional[dict[str, Any]]:
|
||
"""
|
||
提取 SQLite 与 PostgreSQL 驱动提供的稳定错误分类字段。
|
||
"""
|
||
metadata = {"error_type": type(error).__name__}
|
||
|
||
# DBAPI 驱动字段并不共享统一类型,动态读取可同时兼容 sqlite3、psycopg2 与 asyncpg。
|
||
sqlite_errorcode = getattr(error, "sqlite_errorcode", None)
|
||
sqlite_errorname = getattr(error, "sqlite_errorname", None)
|
||
if sqlite_errorcode is not None or sqlite_errorname:
|
||
if sqlite_errorcode is not None:
|
||
metadata["error_code"] = sqlite_errorcode
|
||
if sqlite_errorname:
|
||
metadata["error_name"] = sqlite_errorname
|
||
return metadata
|
||
|
||
sqlstate = getattr(error, "sqlstate", None) or getattr(error, "pgcode", None)
|
||
if not sqlstate:
|
||
sqlstate = getattr(getattr(error, "diag", None), "sqlstate", None)
|
||
if sqlstate:
|
||
metadata["sqlstate"] = sqlstate
|
||
return metadata
|
||
|
||
return None
|
||
|
||
|
||
def _log_database_error(exception_context: ExceptionContext) -> None:
|
||
"""
|
||
记录非敏感驱动错误码,并让 SQLAlchemy 淘汰发生 SQLite I/O 故障的连接。
|
||
|
||
原事务仍向调用方抛错,不在驱动层重放写操作;后续独立事务重新连接。
|
||
锁争用、约束冲突和 PostgreSQL 错误保持方言原有的断连判定。
|
||
"""
|
||
metadata = _database_error_metadata(exception_context.original_exception)
|
||
if not metadata:
|
||
return
|
||
|
||
dialect = exception_context.dialect
|
||
error_code = metadata.get("error_code")
|
||
if (
|
||
dialect.name == "sqlite"
|
||
and isinstance(error_code, int)
|
||
and error_code & _SQLITE_PRIMARY_ERROR_MASK == sqlite3.SQLITE_IOERR
|
||
):
|
||
exception_context.is_disconnect = True
|
||
fields = {
|
||
"database": dialect.name,
|
||
"driver": dialect.driver,
|
||
**metadata,
|
||
}
|
||
logger.error(
|
||
"数据库驱动异常:" + ", ".join(f"{key}={value}" for key, value in fields.items())
|
||
)
|
||
|
||
|
||
def _register_database_error_logging(engine: SQLAlchemyEngine) -> None:
|
||
"""
|
||
为宿主与插件 Engine 注册统一的驱动错误诊断和 SQLite 坏连接失效策略。
|
||
"""
|
||
event.listen(engine, "handle_error", _log_database_error)
|