Files
MoviePilot/app/db/diagnostics.py

80 lines
2.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
数据库错误诊断。
把驱动层的错误分类字段(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)