- 拆分 tencent_cloud_client.py(1311行)为 api_operations.py / image_processor.py / retry.py - 更新 HA 废弃 API: SupportsResponse.OPTIONAL, async_unload_platforms, native_value, 移除 CONNECTION_CLASS - 新增动态人员传感器管理(自动增/删) - 新增 detect_face 服务定义(services.yaml) - 移除 secret_key 持久化存储,增强安全性 - 完善错误映射前缀匹配 + 异常类型安全退避 - 修复 Coordinator 双重人员 ID 跟踪死代码 - 添加 _sanitize_error 脱敏,ImageCache LRU 缓存 - manifest.json 版本 2.1.0 + Pillow 依赖
169 lines
5.4 KiB
Python
169 lines
5.4 KiB
Python
"""重试与执行辅助模块。
|
|
|
|
将原 ``tencent_cloud_client.py`` 中的重试逻辑抽离出来,便于复用与测试。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import random
|
|
import time
|
|
from typing import Any, Callable, Optional
|
|
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import (
|
|
TencentCloudSDKException,
|
|
)
|
|
|
|
from .errors import handle_api_error, log_and_raise_error, ApiError
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
# 重试默认参数
|
|
MAX_ATTEMPTS = 3
|
|
INITIAL_RETRY_DELAY = 1.0
|
|
MAX_RETRY_DELAY = 30.0
|
|
RETRY_BACKOFF_FACTOR = 2.0
|
|
RETRY_JITTER = 0.1
|
|
|
|
# 不可重试的错误码(业务错误,重试无意义)
|
|
NON_RETRYABLE_ERRORS = frozenset({
|
|
"AuthFailure",
|
|
"InvalidParameter",
|
|
"InvalidParameterValue",
|
|
"ResourceNotFound",
|
|
"ResourceNotFoundInUse",
|
|
"FailedOperation.PersonExist",
|
|
"FailedOperation.PersonNotExist",
|
|
"FailedOperation.FaceExist",
|
|
"FailedOperation.FaceNotExist",
|
|
"FailedOperation.GroupExist",
|
|
"FailedOperation.GroupNotExist",
|
|
})
|
|
|
|
# 速率限制类错误码(可重试)
|
|
RATE_LIMIT_ERRORS = frozenset({
|
|
"RequestLimitExceeded",
|
|
"RequestQuotaExceeded",
|
|
"LimitExceeded",
|
|
})
|
|
|
|
# 网络/服务端临时错误码(可重试)
|
|
NETWORK_ERRORS = frozenset({
|
|
"ResourceUnavailable.NetworkError",
|
|
"ResourceUnavailable.ServiceTimeout",
|
|
"InternalError",
|
|
"FailedOperation.RequestTimeout",
|
|
})
|
|
|
|
|
|
class RetryConfig:
|
|
"""重试配置。"""
|
|
|
|
def __init__(
|
|
self,
|
|
max_attempts: int = MAX_ATTEMPTS,
|
|
initial_delay: float = INITIAL_RETRY_DELAY,
|
|
max_delay: float = MAX_RETRY_DELAY,
|
|
backoff_factor: float = RETRY_BACKOFF_FACTOR,
|
|
jitter: float = RETRY_JITTER,
|
|
) -> None:
|
|
self.max_attempts = max(max_attempts, 1)
|
|
self.initial_delay = max(initial_delay, 0.0)
|
|
self.max_delay = max(max_delay, self.initial_delay)
|
|
self.backoff_factor = max(backoff_factor, 1.0)
|
|
self.jitter = max(0.0, min(jitter, 1.0))
|
|
|
|
|
|
def calculate_retry_delay(attempt: int, config: RetryConfig) -> float:
|
|
"""计算第 ``attempt`` 次重试(从 1 开始)的延迟时间。"""
|
|
delay = config.initial_delay * (config.backoff_factor ** (attempt - 1))
|
|
delay = min(delay, config.max_delay)
|
|
jitter = delay * config.jitter
|
|
delay += random.uniform(-jitter, jitter)
|
|
return max(0.0, delay)
|
|
|
|
|
|
def should_retry(exception: TencentCloudSDKException) -> bool:
|
|
"""判断 SDK 异常是否可重试。"""
|
|
code = getattr(exception, "code", "") or ""
|
|
|
|
# 精确匹配不可重试
|
|
if code in NON_RETRYABLE_ERRORS:
|
|
return False
|
|
|
|
# 精确匹配可重试
|
|
if code in RATE_LIMIT_ERRORS or code in NETWORK_ERRORS:
|
|
return True
|
|
|
|
# 前缀匹配:AuthFailure.* / InvalidParameter.* / ResourceNotFound.* 不可重试
|
|
for prefix in ("AuthFailure", "InvalidParameter", "ResourceNotFound"):
|
|
if code.startswith(prefix):
|
|
return False
|
|
|
|
# 其它未知错误默认可重试(服务端临时问题居多)
|
|
return True
|
|
|
|
|
|
def execute_with_retry(
|
|
api_call: Callable[[], Any],
|
|
operation_name: str,
|
|
config: Optional[RetryConfig] = None,
|
|
sanitize_error: Optional[Callable[[str], str]] = None,
|
|
) -> Any:
|
|
"""执行带重试的 API 调用。
|
|
|
|
``api_call`` 应返回业务结果字典(成功)或抛出 ``TencentCloudSDKException``
|
|
/其它异常。当 ``api_call`` 内部已经把异常转化为 ``{success: False}`` 结果时,
|
|
本函数不会重试——重试仅针对抛出的异常。
|
|
|
|
Args:
|
|
api_call: 实际的 API 调用函数。
|
|
operation_name: 操作名称,用于日志。
|
|
config: 重试配置,默认 ``RetryConfig()``。
|
|
sanitize_error: 可选的错误信息脱敏函数。
|
|
"""
|
|
cfg = config or RetryConfig()
|
|
sanitize = sanitize_error or (lambda s: s)
|
|
last_exception: Optional[Exception] = None
|
|
|
|
for attempt in range(cfg.max_attempts):
|
|
try:
|
|
if attempt > 0:
|
|
delay = calculate_retry_delay(attempt, cfg)
|
|
_LOGGER.info(
|
|
"重试第 %d 次 %s,延迟 %.2f 秒", attempt, operation_name, delay
|
|
)
|
|
time.sleep(delay)
|
|
return api_call()
|
|
except TencentCloudSDKException as ex:
|
|
last_exception = ex
|
|
_LOGGER.warning(
|
|
"%s 失败 (尝试 %d/%d): %s",
|
|
operation_name, attempt + 1, cfg.max_attempts, ex.code,
|
|
)
|
|
if not should_retry(ex):
|
|
_LOGGER.error("遇到不可重试的错误: %s", ex.code)
|
|
break
|
|
except Exception as ex: # noqa: BLE001 — 顶层兜底
|
|
last_exception = ex
|
|
_LOGGER.warning(
|
|
"%s 失败 (尝试 %d/%d): %s",
|
|
operation_name, attempt + 1, cfg.max_attempts, sanitize(str(ex)),
|
|
)
|
|
|
|
# 所有尝试均失败
|
|
if isinstance(last_exception, TencentCloudSDKException):
|
|
handle_api_error(last_exception.code, last_exception.message)
|
|
# handle_api_error 总会抛出,这里保险返回
|
|
raise ApiError(f"{operation_name} 失败: {last_exception.code}")
|
|
|
|
if last_exception is not None:
|
|
log_and_raise_error(
|
|
ApiError,
|
|
f"{operation_name} 失败: {last_exception}",
|
|
{"error": str(last_exception), "error_type": type(last_exception).__name__},
|
|
)
|
|
|
|
# 理论不可达
|
|
raise ApiError(f"{operation_name} 失败: 未知错误")
|