85 lines
2.4 KiB
Python
85 lines
2.4 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import logging
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from yuxi.channel.protocols import ClassifiedError, ErrorSeverity, ErrorHandlingProtocol
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
def capture_error(
|
||
|
|
logger_instance: logging.Logger,
|
||
|
|
error: BaseException,
|
||
|
|
*,
|
||
|
|
message: str = "",
|
||
|
|
context: dict[str, Any] | None = None,
|
||
|
|
level: str = "error",
|
||
|
|
) -> ClassifiedError:
|
||
|
|
if level == "warning":
|
||
|
|
logger_instance.warning(
|
||
|
|
"%s: %s | context=%s",
|
||
|
|
message or "Operation failed",
|
||
|
|
error,
|
||
|
|
context or {},
|
||
|
|
exc_info=True,
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
logger_instance.exception(message or "Operation failed")
|
||
|
|
|
||
|
|
return ClassifiedError(
|
||
|
|
severity=_infer_severity(error),
|
||
|
|
original_error=error,
|
||
|
|
error_message=str(error),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def classify_error(error: BaseException) -> ClassifiedError:
|
||
|
|
return ClassifiedError(
|
||
|
|
severity=_infer_severity(error),
|
||
|
|
original_error=error,
|
||
|
|
error_message=str(error),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def is_retryable(error: BaseException) -> bool:
|
||
|
|
return _infer_severity(error) == ErrorSeverity.RETRYABLE
|
||
|
|
|
||
|
|
|
||
|
|
def should_backoff(error: BaseException, attempt: int) -> int:
|
||
|
|
if not is_retryable(error):
|
||
|
|
return 0
|
||
|
|
base_ms = 1000
|
||
|
|
max_ms = 30000
|
||
|
|
delay_ms = min(base_ms * (2 ** (attempt - 1)), max_ms)
|
||
|
|
return delay_ms
|
||
|
|
|
||
|
|
|
||
|
|
def _infer_severity(error: BaseException) -> ErrorSeverity:
|
||
|
|
import asyncio
|
||
|
|
import builtins
|
||
|
|
|
||
|
|
if isinstance(error, asyncio.TimeoutError):
|
||
|
|
return ErrorSeverity.NETWORK
|
||
|
|
if isinstance(error, builtins.TimeoutError):
|
||
|
|
return ErrorSeverity.NETWORK
|
||
|
|
if isinstance(error, ConnectionError):
|
||
|
|
return ErrorSeverity.NETWORK
|
||
|
|
if isinstance(error, PermissionError):
|
||
|
|
return ErrorSeverity.FORBIDDEN
|
||
|
|
if isinstance(error, (ValueError, TypeError, KeyError, IndexError, AttributeError)):
|
||
|
|
return ErrorSeverity.FATAL
|
||
|
|
if isinstance(error, asyncio.CancelledError):
|
||
|
|
return ErrorSeverity.FATAL
|
||
|
|
return ErrorSeverity.RETRYABLE
|
||
|
|
|
||
|
|
|
||
|
|
class DefaultErrorHandler(ErrorHandlingProtocol):
|
||
|
|
def classify_error(self, error: BaseException) -> ClassifiedError:
|
||
|
|
return classify_error(error)
|
||
|
|
|
||
|
|
def is_retryable(self, error: BaseException) -> bool:
|
||
|
|
return is_retryable(error)
|
||
|
|
|
||
|
|
def should_backoff(self, error: BaseException, attempt: int) -> int:
|
||
|
|
return should_backoff(error, attempt)
|