467 lines
15 KiB
Python
467 lines
15 KiB
Python
"""
|
|
Error handling utilities for the ComfyUI Queue Manager.
|
|
Provides centralized error handling, logging, and recovery mechanisms.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import logging
|
|
import sqlite3
|
|
import traceback
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Callable, Dict, Optional, Type, TypeVar, Union
|
|
|
|
from exceptions import (
|
|
APIError,
|
|
ConnectionError,
|
|
DatabaseError,
|
|
QueueManagerError,
|
|
QueueServiceError,
|
|
SchemaError,
|
|
TimeoutError,
|
|
ValidationError,
|
|
WorkflowExecutionError,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
T = TypeVar('T')
|
|
|
|
|
|
class ErrorHandler:
|
|
"""Centralized error handling and recovery utilities."""
|
|
|
|
@staticmethod
|
|
def handle_database_error(
|
|
error: Exception,
|
|
operation: str,
|
|
table: Optional[str] = None,
|
|
context: Optional[Dict[str, Any]] = None
|
|
) -> DatabaseError:
|
|
"""Handle and convert database errors to structured exceptions.
|
|
|
|
Args:
|
|
error: Original exception
|
|
operation: Database operation that failed
|
|
table: Database table involved
|
|
context: Additional context information
|
|
|
|
Returns:
|
|
Structured DatabaseError
|
|
"""
|
|
context = context or {}
|
|
|
|
# Handle SQLite-specific errors
|
|
if isinstance(error, sqlite3.Error):
|
|
if isinstance(error, sqlite3.OperationalError):
|
|
error_msg = str(error).lower()
|
|
|
|
if "database is locked" in error_msg:
|
|
return ConnectionError(
|
|
f"Database is locked during {operation}",
|
|
operation=operation,
|
|
table=table,
|
|
details=context,
|
|
cause=error
|
|
)
|
|
elif "no such table" in error_msg:
|
|
return SchemaError(
|
|
f"Table does not exist during {operation}",
|
|
operation=operation,
|
|
table=table,
|
|
details=context,
|
|
cause=error
|
|
)
|
|
elif "disk i/o error" in error_msg:
|
|
return DatabaseError(
|
|
f"Disk I/O error during {operation}",
|
|
error_code="DB_IO_ERROR",
|
|
operation=operation,
|
|
table=table,
|
|
details=context,
|
|
cause=error
|
|
)
|
|
else:
|
|
return DatabaseError(
|
|
f"Database operational error during {operation}: {error}",
|
|
error_code="DB_OPERATIONAL_ERROR",
|
|
operation=operation,
|
|
table=table,
|
|
details=context,
|
|
cause=error
|
|
)
|
|
|
|
elif isinstance(error, sqlite3.IntegrityError):
|
|
return ValidationError(
|
|
f"Data integrity violation during {operation}: {error}",
|
|
details={**context, "operation": operation, "table": table},
|
|
cause=error
|
|
)
|
|
|
|
elif isinstance(error, sqlite3.DatabaseError):
|
|
return DatabaseError(
|
|
f"Database error during {operation}: {error}",
|
|
error_code="DB_GENERAL_ERROR",
|
|
operation=operation,
|
|
table=table,
|
|
details=context,
|
|
cause=error
|
|
)
|
|
|
|
# Handle general exceptions
|
|
return DatabaseError(
|
|
f"Unexpected error during {operation}: {error}",
|
|
error_code="DB_UNEXPECTED_ERROR",
|
|
operation=operation,
|
|
table=table,
|
|
details=context,
|
|
cause=error
|
|
)
|
|
|
|
@staticmethod
|
|
def handle_api_error(
|
|
error: Exception,
|
|
endpoint: str,
|
|
method: str = "GET",
|
|
context: Optional[Dict[str, Any]] = None
|
|
) -> APIError:
|
|
"""Handle and convert API errors to structured exceptions.
|
|
|
|
Args:
|
|
error: Original exception
|
|
endpoint: API endpoint that failed
|
|
method: HTTP method
|
|
context: Additional context information
|
|
|
|
Returns:
|
|
Structured APIError
|
|
"""
|
|
context = context or {}
|
|
context.update({"endpoint": endpoint, "method": method})
|
|
|
|
if isinstance(error, QueueManagerError):
|
|
# Convert existing queue manager errors to API errors
|
|
return APIError(
|
|
error.message,
|
|
status_code=500,
|
|
endpoint=endpoint,
|
|
details=context,
|
|
cause=error
|
|
)
|
|
|
|
return APIError(
|
|
f"API error at {method} {endpoint}: {error}",
|
|
status_code=500,
|
|
endpoint=endpoint,
|
|
details=context,
|
|
cause=error
|
|
)
|
|
|
|
@staticmethod
|
|
def handle_workflow_error(
|
|
error: Exception,
|
|
workflow_id: Optional[str] = None,
|
|
stage: Optional[str] = None,
|
|
context: Optional[Dict[str, Any]] = None
|
|
) -> WorkflowExecutionError:
|
|
"""Handle and convert workflow execution errors.
|
|
|
|
Args:
|
|
error: Original exception
|
|
workflow_id: ID of the workflow that failed
|
|
stage: Execution stage where error occurred
|
|
context: Additional context information
|
|
|
|
Returns:
|
|
Structured WorkflowExecutionError
|
|
"""
|
|
context = context or {}
|
|
|
|
return WorkflowExecutionError(
|
|
f"Workflow execution failed: {error}",
|
|
workflow_id=workflow_id,
|
|
execution_stage=stage,
|
|
details=context,
|
|
cause=error
|
|
)
|
|
|
|
@staticmethod
|
|
def log_error(
|
|
error: Exception,
|
|
context: Optional[Dict[str, Any]] = None,
|
|
level: int = logging.ERROR
|
|
) -> None:
|
|
"""Log an error with structured information.
|
|
|
|
Args:
|
|
error: Exception to log
|
|
context: Additional context information
|
|
level: Logging level
|
|
"""
|
|
context = context or {}
|
|
|
|
if isinstance(error, QueueManagerError):
|
|
# Log structured queue manager errors
|
|
log_data = {
|
|
"error_type": error.__class__.__name__,
|
|
"error_code": error.error_code,
|
|
"message": error.message,
|
|
"details": error.details,
|
|
**context
|
|
}
|
|
|
|
logger.log(level, f"Queue Manager Error: {error.message}", extra=log_data)
|
|
|
|
if error.cause:
|
|
logger.log(level, f"Caused by: {error.cause}", exc_info=error.cause)
|
|
else:
|
|
# Log general exceptions
|
|
logger.log(level, f"Unexpected error: {error}", extra=context, exc_info=error)
|
|
|
|
|
|
def with_error_handling(
|
|
error_type: Type[QueueManagerError] = QueueManagerError,
|
|
operation: Optional[str] = None,
|
|
log_errors: bool = True,
|
|
reraise: bool = True
|
|
):
|
|
"""Decorator for automatic error handling and logging.
|
|
|
|
Args:
|
|
error_type: Type of exception to convert errors to
|
|
operation: Name of the operation for context
|
|
log_errors: Whether to log errors automatically
|
|
reraise: Whether to reraise the converted exception
|
|
"""
|
|
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
@functools.wraps(func)
|
|
def wrapper(*args, **kwargs) -> T:
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except QueueManagerError:
|
|
# Re-raise queue manager errors as-is
|
|
raise
|
|
except Exception as e:
|
|
# Convert other exceptions
|
|
operation_name = operation or func.__name__
|
|
|
|
if error_type == DatabaseError:
|
|
converted_error = ErrorHandler.handle_database_error(
|
|
e, operation_name, context={"function": func.__name__}
|
|
)
|
|
elif error_type == APIError:
|
|
converted_error = ErrorHandler.handle_api_error(
|
|
e, operation_name, context={"function": func.__name__}
|
|
)
|
|
elif error_type == WorkflowExecutionError:
|
|
converted_error = ErrorHandler.handle_workflow_error(
|
|
e, context={"function": func.__name__}
|
|
)
|
|
else:
|
|
converted_error = error_type(
|
|
f"Error in {operation_name}: {e}",
|
|
details={"function": func.__name__},
|
|
cause=e
|
|
)
|
|
|
|
if log_errors:
|
|
ErrorHandler.log_error(converted_error)
|
|
|
|
if reraise:
|
|
raise converted_error
|
|
|
|
return converted_error
|
|
|
|
return wrapper
|
|
return decorator
|
|
|
|
|
|
def with_database_error_handling(
|
|
operation: Optional[str] = None,
|
|
table: Optional[str] = None,
|
|
log_errors: bool = True
|
|
):
|
|
"""Decorator specifically for database operations.
|
|
|
|
Args:
|
|
operation: Database operation name
|
|
table: Database table name
|
|
log_errors: Whether to log errors automatically
|
|
"""
|
|
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
@functools.wraps(func)
|
|
def wrapper(*args, **kwargs) -> T:
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except QueueManagerError:
|
|
# Re-raise queue manager errors as-is
|
|
raise
|
|
except Exception as e:
|
|
operation_name = operation or func.__name__
|
|
converted_error = ErrorHandler.handle_database_error(
|
|
e,
|
|
operation_name,
|
|
table=table,
|
|
context={"function": func.__name__}
|
|
)
|
|
|
|
if log_errors:
|
|
ErrorHandler.log_error(converted_error)
|
|
|
|
raise converted_error
|
|
|
|
return wrapper
|
|
return decorator
|
|
|
|
|
|
def with_api_error_handling(
|
|
endpoint: Optional[str] = None,
|
|
method: str = "GET",
|
|
log_errors: bool = True
|
|
):
|
|
"""Decorator specifically for API operations.
|
|
|
|
Args:
|
|
endpoint: API endpoint name
|
|
method: HTTP method
|
|
log_errors: Whether to log errors automatically
|
|
"""
|
|
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
@functools.wraps(func)
|
|
def wrapper(*args, **kwargs) -> T:
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except QueueManagerError:
|
|
# Re-raise queue manager errors as-is
|
|
raise
|
|
except Exception as e:
|
|
endpoint_name = endpoint or func.__name__
|
|
converted_error = ErrorHandler.handle_api_error(
|
|
e,
|
|
endpoint_name,
|
|
method=method,
|
|
context={"function": func.__name__}
|
|
)
|
|
|
|
if log_errors:
|
|
ErrorHandler.log_error(converted_error)
|
|
|
|
raise converted_error
|
|
|
|
return wrapper
|
|
return decorator
|
|
|
|
|
|
class ErrorRecovery:
|
|
"""Utilities for error recovery and retry mechanisms."""
|
|
|
|
@staticmethod
|
|
def retry_with_backoff(
|
|
func: Callable[..., T],
|
|
max_retries: int = 3,
|
|
base_delay: float = 1.0,
|
|
max_delay: float = 60.0,
|
|
backoff_factor: float = 2.0,
|
|
exceptions: tuple = (Exception,)
|
|
) -> T:
|
|
"""Retry a function with exponential backoff.
|
|
|
|
Args:
|
|
func: Function to retry
|
|
max_retries: Maximum number of retry attempts
|
|
base_delay: Initial delay between retries
|
|
max_delay: Maximum delay between retries
|
|
backoff_factor: Factor to multiply delay by each retry
|
|
exceptions: Tuple of exceptions to catch and retry on
|
|
|
|
Returns:
|
|
Result of the function call
|
|
|
|
Raises:
|
|
The last exception if all retries fail
|
|
"""
|
|
import time
|
|
|
|
last_exception = None
|
|
delay = base_delay
|
|
|
|
for attempt in range(max_retries + 1):
|
|
try:
|
|
return func()
|
|
except exceptions as e:
|
|
last_exception = e
|
|
|
|
if attempt == max_retries:
|
|
# Last attempt failed, re-raise
|
|
break
|
|
|
|
logger.warning(
|
|
f"Attempt {attempt + 1} failed, retrying in {delay:.2f}s: {e}"
|
|
)
|
|
|
|
time.sleep(delay)
|
|
delay = min(delay * backoff_factor, max_delay)
|
|
|
|
# All retries failed
|
|
if last_exception:
|
|
raise last_exception
|
|
else:
|
|
raise RuntimeError("All retry attempts failed")
|
|
|
|
@staticmethod
|
|
def safe_execute(
|
|
func: Callable[..., T],
|
|
default_value: Optional[T] = None,
|
|
log_errors: bool = True,
|
|
exceptions: tuple = (Exception,)
|
|
) -> Optional[T]:
|
|
"""Execute a function safely, returning a default value on error.
|
|
|
|
Args:
|
|
func: Function to execute
|
|
default_value: Value to return on error
|
|
log_errors: Whether to log errors
|
|
exceptions: Tuple of exceptions to catch
|
|
|
|
Returns:
|
|
Function result or default value
|
|
"""
|
|
try:
|
|
return func()
|
|
except exceptions as e:
|
|
if log_errors:
|
|
logger.error(f"Safe execution failed: {e}", exc_info=True)
|
|
return default_value
|
|
|
|
|
|
def create_error_response(
|
|
error: Exception,
|
|
include_traceback: bool = False
|
|
) -> Dict[str, Any]:
|
|
"""Create a standardized error response dictionary.
|
|
|
|
Args:
|
|
error: Exception to convert to response
|
|
include_traceback: Whether to include traceback in response
|
|
|
|
Returns:
|
|
Dictionary containing error information
|
|
"""
|
|
if isinstance(error, QueueManagerError):
|
|
response = error.to_dict()
|
|
else:
|
|
response = {
|
|
"error_type": error.__class__.__name__,
|
|
"error_code": "UNEXPECTED_ERROR",
|
|
"message": str(error),
|
|
"details": {},
|
|
"cause": None
|
|
}
|
|
|
|
response["timestamp"] = datetime.now(timezone.utc).isoformat()
|
|
|
|
if include_traceback:
|
|
response["traceback"] = traceback.format_exc()
|
|
|
|
return response |