mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-19 14:55:40 +08:00
175 lines
6.0 KiB
Python
175 lines
6.0 KiB
Python
#
|
|
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
"""GaussDB metadata adapter exception classification helpers.
|
|
|
|
This module deliberately avoids importing ``db_models`` or service classes so
|
|
the GaussDB pool, migrations, and retry decorator do not form import cycles.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
|
|
|
|
# Connection-related SQLSTATE values reported by GaussDB/PostgreSQL/libpq.
|
|
# GaussDB exposes errors through psycopg2/libpq, so connection failures and
|
|
# server rejections should be classified by SQLSTATE instead of English error
|
|
# text. Text matching is only a fallback when the driver omits pgcode. The
|
|
# 08xxx class covers connection exceptions, while 57P0x covers shutdowns and
|
|
# temporarily unavailable servers.
|
|
PSYCOPG_CONNECTION_SQLSTATES = {
|
|
"08000", # connection_exception
|
|
"08001", # sqlclient_unable_to_establish_sqlconnection
|
|
"08003", # connection_does_not_exist
|
|
"08004", # sqlserver_rejected_establishment_of_sqlconnection
|
|
"08006", # connection_failure
|
|
"08007", # transaction_resolution_unknown
|
|
"08P01", # protocol_violation
|
|
"57P01", # admin_shutdown
|
|
"57P02", # crash_shutdown
|
|
"57P03", # cannot_connect_now
|
|
}
|
|
|
|
# Transaction-conflict SQLSTATE values observed on GaussDB:
|
|
# 40P01: deadlock detected;
|
|
# 40001: could not serialize access due to concurrent update;
|
|
# 55P03: FOR UPDATE NOWAIT could not acquire the row lock.
|
|
# Each indicates a statement or transaction that can be retried as a complete
|
|
# RAGFlow database operation.
|
|
PSYCOPG_RETRYABLE_TRANSACTION_SQLSTATES = {"40P01", "40001", "55P03"}
|
|
|
|
# SQLSTATE values used to make DDL migrations idempotent. Migration code must
|
|
# distinguish an already-applied change from a real failure. GaussDB reports
|
|
# duplicate columns as 42701, duplicate table/index names as 42P07, and missing
|
|
# index/constraint objects as 42704.
|
|
PSYCOPG_DUPLICATE_COLUMN_SQLSTATES = {"42701"}
|
|
PSYCOPG_DUPLICATE_OBJECT_SQLSTATES = {"42P07"}
|
|
PSYCOPG_UNDEFINED_OBJECT_SQLSTATES = {"42704"}
|
|
|
|
CONNECTION_ERROR_MESSAGES = (
|
|
"server closed",
|
|
"connection refused",
|
|
"no connection to the server",
|
|
"terminating connection",
|
|
"ssl connection has been closed unexpectedly",
|
|
"ssl syscall error: eof detected",
|
|
)
|
|
|
|
|
|
def _iter_exception_chain(exc: BaseException) -> Iterable[BaseException | object]:
|
|
"""Iterate through Peewee wrappers, chained errors, and driver errors in args."""
|
|
seen: set[int] = set()
|
|
stack: list[BaseException | object] = [exc]
|
|
while stack:
|
|
current = stack.pop(0)
|
|
current_id = id(current)
|
|
if current_id in seen:
|
|
continue
|
|
seen.add(current_id)
|
|
yield current
|
|
|
|
for attr in ("__cause__", "__context__", "orig", "original_exception"):
|
|
nested = getattr(current, attr, None)
|
|
if nested is not None:
|
|
stack.append(nested)
|
|
|
|
for arg in getattr(current, "args", ()) or ():
|
|
if hasattr(arg, "pgcode"):
|
|
stack.append(arg)
|
|
|
|
|
|
def sqlstate_from_exception(exc: BaseException) -> str | None:
|
|
"""Extract SQLSTATE from a psycopg2 error or a Peewee-wrapped error."""
|
|
for current in _iter_exception_chain(exc):
|
|
pgcode = getattr(current, "pgcode", None)
|
|
if pgcode:
|
|
return str(pgcode)
|
|
|
|
diag = getattr(current, "diag", None)
|
|
sqlstate = getattr(diag, "sqlstate", None) if diag else None
|
|
if sqlstate:
|
|
return str(sqlstate)
|
|
|
|
return None
|
|
|
|
|
|
def exception_text(exc: BaseException) -> str:
|
|
return str(exc).lower()
|
|
|
|
|
|
def mysql_errno_from_exception(exc: BaseException) -> int | None:
|
|
args = getattr(exc, "args", ())
|
|
if args and isinstance(args[0], int):
|
|
return args[0]
|
|
return None
|
|
|
|
|
|
def is_psycopg_connection_error(exc: BaseException) -> bool:
|
|
sqlstate = sqlstate_from_exception(exc)
|
|
if sqlstate is not None:
|
|
return sqlstate.startswith("08") or sqlstate in {"57P01", "57P02", "57P03"}
|
|
|
|
if exc.__class__.__name__ == "InterfaceError":
|
|
return True
|
|
|
|
text = exception_text(exc)
|
|
return any(message in text for message in CONNECTION_ERROR_MESSAGES)
|
|
|
|
|
|
def is_retryable_transaction_error(exc: BaseException) -> bool:
|
|
sqlstate = sqlstate_from_exception(exc)
|
|
if sqlstate in PSYCOPG_RETRYABLE_TRANSACTION_SQLSTATES:
|
|
return True
|
|
|
|
text = exception_text(exc)
|
|
return "deadlock detected" in text or "could not serialize access" in text or "could not obtain lock" in text
|
|
|
|
|
|
def is_duplicate_column_error(exc: BaseException) -> bool:
|
|
if mysql_errno_from_exception(exc) == 1060:
|
|
return True
|
|
|
|
sqlstate = sqlstate_from_exception(exc)
|
|
if sqlstate in PSYCOPG_DUPLICATE_COLUMN_SQLSTATES:
|
|
return True
|
|
|
|
text = exception_text(exc)
|
|
return "duplicate column" in text or ("column" in text and "already exists" in text)
|
|
|
|
|
|
def is_duplicate_object_error(exc: BaseException) -> bool:
|
|
if mysql_errno_from_exception(exc) == 1061:
|
|
return True
|
|
|
|
sqlstate = sqlstate_from_exception(exc)
|
|
if sqlstate in PSYCOPG_DUPLICATE_OBJECT_SQLSTATES:
|
|
return True
|
|
|
|
text = exception_text(exc)
|
|
return "duplicate key name" in text or "already exists" in text
|
|
|
|
|
|
def is_undefined_object_error(exc: BaseException) -> bool:
|
|
if mysql_errno_from_exception(exc) == 1091:
|
|
return True
|
|
|
|
sqlstate = sqlstate_from_exception(exc)
|
|
if sqlstate in PSYCOPG_UNDEFINED_OBJECT_SQLSTATES:
|
|
return True
|
|
|
|
text = exception_text(exc)
|
|
return "can't drop" in text or "does not exist" in text
|