Files
wehub-resource-sync 41b710f9c7
CI / Frontend checks (push) Failing after 0s
CI / Backend tests (push) Failing after 1s
I18n Documentation Sync / sync-docs (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:28:40 +08:00

320 lines
12 KiB
Python

"""
分层错误处理系统 - 提供统一的错误处理、重试机制和熔断器
"""
import logging
import time
import functools
from typing import Type, Callable, Any, Optional, Dict, List
from enum import Enum
from dataclasses import dataclass
from contextlib import contextmanager
logger = logging.getLogger(__name__)
class ErrorLevel(Enum):
"""错误级别枚举"""
DEBUG = "DEBUG"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
CRITICAL = "CRITICAL"
class ErrorCategory(Enum):
"""错误分类枚举"""
CONFIGURATION = "CONFIGURATION"
NETWORK = "NETWORK"
API = "API"
FILE_IO = "FILE_IO"
PROCESSING = "PROCESSING"
VALIDATION = "VALIDATION"
SYSTEM = "SYSTEM"
class AutoClipsException(Exception):
"""自动切片工具基础异常类"""
def __init__(self, message: str, category: ErrorCategory, level: ErrorLevel = ErrorLevel.ERROR,
details: Optional[Dict[str, Any]] = None, original_exception: Optional[Exception] = None):
super().__init__(message)
self.message = message
self.category = category
self.level = level
self.details = details or {}
self.original_exception = original_exception
self.timestamp = time.time()
def __str__(self):
return f"[{self.category.value}] {self.message}"
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
"message": self.message,
"category": self.category.value,
"level": self.level.value,
"details": self.details,
"timestamp": self.timestamp,
"original_exception": str(self.original_exception) if self.original_exception else None
}
class ConfigurationError(AutoClipsException):
"""配置错误"""
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
super().__init__(message, ErrorCategory.CONFIGURATION, ErrorLevel.ERROR, details)
class NetworkError(AutoClipsException):
"""网络错误"""
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None, original_exception: Optional[Exception] = None):
super().__init__(message, ErrorCategory.NETWORK, ErrorLevel.ERROR, details, original_exception)
class APIError(AutoClipsException):
"""API调用错误"""
def __init__(self, message: str, status_code: Optional[int] = None, details: Optional[Dict[str, Any]] = None):
api_details = details or {}
if status_code:
api_details["status_code"] = status_code
super().__init__(message, ErrorCategory.API, ErrorLevel.ERROR, api_details)
class FileIOError(AutoClipsException):
"""文件IO错误"""
def __init__(self, message: str, file_path: Optional[str] = None, details: Optional[Dict[str, Any]] = None):
file_details = details or {}
if file_path:
file_details["file_path"] = file_path
super().__init__(message, ErrorCategory.FILE_IO, ErrorLevel.ERROR, file_details)
class ProcessingError(AutoClipsException):
"""处理错误"""
def __init__(self, message: str, step: Optional[str] = None, details: Optional[Dict[str, Any]] = None):
processing_details = details or {}
if step:
processing_details["step"] = step
super().__init__(message, ErrorCategory.PROCESSING, ErrorLevel.ERROR, processing_details)
class ValidationError(AutoClipsException):
"""验证错误"""
def __init__(self, message: str, field: Optional[str] = None, details: Optional[Dict[str, Any]] = None):
validation_details = details or {}
if field:
validation_details["field"] = field
super().__init__(message, ErrorCategory.VALIDATION, ErrorLevel.WARNING, validation_details)
@dataclass
class RetryConfig:
"""重试配置"""
max_retries: int = 3
base_delay: float = 1.0
max_delay: float = 60.0
exponential_base: float = 2.0
retryable_exceptions: List[Type[Exception]] = None
def __post_init__(self):
if self.retryable_exceptions is None:
self.retryable_exceptions = [
NetworkError,
APIError,
ConnectionError,
TimeoutError,
OSError
]
class CircuitBreaker:
"""熔断器实现"""
def __init__(self, failure_threshold: int = 5, recovery_timeout: float = 60.0,
expected_exception: Type[Exception] = Exception):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.expected_exception = expected_exception
self.failure_count = 0
self.last_failure_time = 0
self.state = "CLOSED" # CLOSED, OPEN, HALF_OPEN
def call(self, func: Callable, *args, **kwargs) -> Any:
"""执行函数,应用熔断器逻辑"""
if self.state == "OPEN":
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = "HALF_OPEN"
else:
raise AutoClipsException(
"熔断器处于开启状态,拒绝执行",
ErrorCategory.SYSTEM,
ErrorLevel.WARNING
)
try:
result = func(*args, **kwargs)
if self.state == "HALF_OPEN":
self.state = "CLOSED"
self.failure_count = 0
return result
except self.expected_exception as e:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "OPEN"
raise e
def retry_with_backoff(config: Optional[RetryConfig] = None):
"""重试装饰器,支持指数退避"""
if config is None:
config = RetryConfig()
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(config.max_retries + 1):
try:
return func(*args, **kwargs)
except tuple(config.retryable_exceptions) as e:
last_exception = e
if attempt == config.max_retries:
logger.error(f"函数 {func.__name__}{config.max_retries} 次重试后失败: {e}")
raise e
# 计算延迟时间
delay = min(
config.base_delay * (config.exponential_base ** attempt),
config.max_delay
)
logger.warning(f"函数 {func.__name__}{attempt + 1} 次尝试失败,{delay}秒后重试: {e}")
time.sleep(delay)
if last_exception:
raise last_exception
return wrapper
return decorator
@contextmanager
def error_context(category: ErrorCategory, context_info: Optional[Dict[str, Any]] = None):
"""错误上下文管理器"""
try:
yield
except Exception as e:
if isinstance(e, AutoClipsException):
# 已经是自定义异常,直接抛出
raise
else:
# 转换为自定义异常
details = context_info or {}
details["original_exception_type"] = type(e).__name__
if category == ErrorCategory.API:
raise APIError(str(e), details=details)
elif category == ErrorCategory.NETWORK:
raise NetworkError(str(e), details=details, original_exception=e)
elif category == ErrorCategory.FILE_IO:
raise FileIOError(str(e), details=details)
elif category == ErrorCategory.PROCESSING:
raise ProcessingError(str(e), details=details)
elif category == ErrorCategory.VALIDATION:
raise ValidationError(str(e), details=details)
else:
raise AutoClipsException(str(e), category, details=details, original_exception=e)
class ErrorHandler:
"""错误处理器"""
def __init__(self):
self.error_log: List[AutoClipsException] = []
self.circuit_breakers: Dict[str, CircuitBreaker] = {}
def handle_error(self, error: AutoClipsException, context: Optional[str] = None):
"""处理错误"""
# 记录错误
self.error_log.append(error)
# 根据错误级别记录日志
if error.level == ErrorLevel.DEBUG:
logger.debug(f"[{context}] {error}")
elif error.level == ErrorLevel.INFO:
logger.info(f"[{context}] {error}")
elif error.level == ErrorLevel.WARNING:
logger.warning(f"[{context}] {error}")
elif error.level == ErrorLevel.ERROR:
logger.error(f"[{context}] {error}")
elif error.level == ErrorLevel.CRITICAL:
logger.critical(f"[{context}] {error}")
# 根据错误分类进行特殊处理
if error.category == ErrorCategory.API and isinstance(error, APIError):
self._handle_api_error(error)
elif error.category == ErrorCategory.NETWORK and isinstance(error, NetworkError):
self._handle_network_error(error)
elif error.category == ErrorCategory.CONFIGURATION and isinstance(error, ConfigurationError):
self._handle_configuration_error(error)
def _handle_api_error(self, error: APIError):
"""处理API错误"""
# 可以在这里添加API错误的具体处理逻辑
# 比如更新API密钥、切换备用API等
pass
def _handle_network_error(self, error: NetworkError):
"""处理网络错误"""
# 可以在这里添加网络错误的具体处理逻辑
# 比如切换网络、重试连接等
pass
def _handle_configuration_error(self, error: ConfigurationError):
"""处理配置错误"""
# 可以在这里添加配置错误的具体处理逻辑
# 比如加载默认配置、提示用户修复等
pass
def get_circuit_breaker(self, name: str, **kwargs) -> CircuitBreaker:
"""获取或创建熔断器"""
if name not in self.circuit_breakers:
self.circuit_breakers[name] = CircuitBreaker(**kwargs)
return self.circuit_breakers[name]
def get_error_summary(self) -> Dict[str, Any]:
"""获取错误摘要"""
if not self.error_log:
return {"total_errors": 0}
error_counts = {}
for error in self.error_log:
category = error.category.value
error_counts[category] = error_counts.get(category, 0) + 1
return {
"total_errors": len(self.error_log),
"error_counts": error_counts,
"latest_error": self.error_log[-1].to_dict() if self.error_log else None
}
def clear_error_log(self):
"""清空错误日志"""
self.error_log.clear()
# 全局错误处理器实例
error_handler = ErrorHandler()
def safe_execute(func: Callable, *args, context: Optional[str] = None,
retry_config: Optional[RetryConfig] = None, **kwargs) -> Any:
"""安全执行函数,包含错误处理和重试"""
if retry_config:
func = retry_with_backoff(retry_config)(func)
try:
return func(*args, **kwargs)
except AutoClipsException as e:
error_handler.handle_error(e, context)
raise
except Exception as e:
# 转换为通用异常
auto_clips_error = AutoClipsException(
str(e),
ErrorCategory.SYSTEM,
ErrorLevel.ERROR,
original_exception=e
)
error_handler.handle_error(auto_clips_error, context)
raise auto_clips_error