""" Custom exception classes for the owned clip analysis backfill job. This module provides a hierarchy of exceptions that enable more precise error handling and recovery strategies than generic Exception catching. """ from typing import Optional, Any, List class BackfillJobError(Exception): """Base exception for all backfill job errors.""" def __init__(self, message: str, error_code: Optional[str] = None, **context): """ Initialize the exception with context. Args: message: Human-readable error message error_code: Optional error code for programmatic handling **context: Additional context information for debugging """ super().__init__(message) self.error_code = error_code self.context = context def __str__(self) -> str: """Return a detailed string representation.""" base_msg = super().__str__() if self.error_code: base_msg = f"[{self.error_code}] {base_msg}" if self.context: context_str = ", ".join(f"{k}={v}" for k, v in self.context.items()) base_msg = f"{base_msg} (context: {context_str})" return base_msg class ConfigurationError(BackfillJobError): """Raised when there are configuration validation errors.""" pass class DatabaseError(BackfillJobError): """Base class for database-related errors.""" pass class ConnectionError(DatabaseError): """Raised when database connections fail or are lost.""" def __init__(self, message: str, connection_type: str = "unknown", **context): """ Initialize with connection type information. Args: message: Error message connection_type: Type of connection (read/write) **context: Additional context """ super().__init__(message, error_code="CONNECTION_ERROR", **context) self.connection_type = connection_type class DeadlockError(DatabaseError): """Raised when database deadlocks are detected.""" def __init__(self, message: str, retry_count: int = 0, **context): """ Initialize with retry information. Args: message: Error message retry_count: Current retry attempt number **context: Additional context """ super().__init__(message, error_code="DEADLOCK_ERROR", retry_count=retry_count, **context) self.retry_count = retry_count class ForeignKeyConstraintError(DatabaseError): """Raised when foreign key constraints are violated.""" def __init__(self, message: str, table_name: Optional[str] = None, constraint_name: Optional[str] = None, **context): """ Initialize with constraint information. Args: message: Error message table_name: Table where constraint was violated constraint_name: Name of the violated constraint **context: Additional context """ super().__init__( message, error_code="FK_CONSTRAINT_ERROR", table_name=table_name, constraint_name=constraint_name, **context ) self.table_name = table_name self.constraint_name = constraint_name class RaceConditionError(DatabaseError): """Raised when race conditions are detected (e.g., clips deleted between validation and insert).""" def __init__(self, message: str, affected_records: int = 0, **context): """ Initialize with race condition information. Args: message: Error message affected_records: Number of records affected by the race condition **context: Additional context """ super().__init__( message, error_code="RACE_CONDITION_ERROR", affected_records=affected_records, **context ) self.affected_records = affected_records class ValidationError(BackfillJobError): """Raised when data validation fails.""" def __init__(self, message: str, validation_type: str = "unknown", invalid_records: Optional[List[Any]] = None, **context): """ Initialize with validation information. Args: message: Error message validation_type: Type of validation that failed invalid_records: List of records that failed validation **context: Additional context """ super().__init__( message, error_code="VALIDATION_ERROR", validation_type=validation_type, **context ) self.validation_type = validation_type self.invalid_records = invalid_records or [] class S3Error(BackfillJobError): """Raised when S3 operations fail.""" def __init__(self, message: str, operation: str = "unknown", bucket: Optional[str] = None, key: Optional[str] = None, **context): """ Initialize with S3 operation information. Args: message: Error message operation: S3 operation that failed (list, get, etc.) bucket: S3 bucket name key: S3 key/prefix **context: Additional context """ super().__init__( message, error_code="S3_ERROR", operation=operation, bucket=bucket, key=key, **context ) self.operation = operation self.bucket = bucket self.key = key class BatchProcessingError(BackfillJobError): """Raised when batch processing operations fail.""" def __init__(self, message: str, batch_size: int = 0, partition_id: Optional[str] = None, **context): """ Initialize with batch processing information. Args: message: Error message batch_size: Size of the batch being processed partition_id: ID of the partition being processed **context: Additional context """ super().__init__( message, error_code="BATCH_PROCESSING_ERROR", batch_size=batch_size, partition_id=partition_id, **context ) self.batch_size = batch_size self.partition_id = partition_id class RetryExhaustedException(BackfillJobError): """Raised when maximum retry attempts have been exhausted.""" def __init__(self, message: str, max_retries: int, last_error: Optional[Exception] = None, **context): """ Initialize with retry information. Args: message: Error message max_retries: Maximum number of retries that were attempted last_error: The last exception that caused the retry failure **context: Additional context """ super().__init__( message, error_code="RETRY_EXHAUSTED", max_retries=max_retries, **context ) self.max_retries = max_retries self.last_error = last_error def classify_database_error(exception: Exception, context: Optional[dict] = None) -> DatabaseError: """ Classify a generic database exception into a specific error type. This function examines the exception message and type to determine the most appropriate custom exception class to use. Args: exception: The original exception to classify context: Optional additional context information Returns: A specific DatabaseError subclass instance """ error_msg = str(exception).lower() context = context or {} if "deadlock" in error_msg or "lock" in error_msg: return DeadlockError(str(exception), **context) elif "ssl" in error_msg or "connection" in error_msg: return ConnectionError(str(exception), **context) elif "foreign key" in error_msg or "violates foreign key constraint" in error_msg: return ForeignKeyConstraintError(str(exception), **context) else: return DatabaseError(str(exception), error_code="GENERIC_DB_ERROR", **context) def is_retryable_error(exception: Exception) -> bool: """ Determine if an exception represents a retryable condition. Args: exception: The exception to check Returns: True if the error condition is likely transient and worth retrying """ if isinstance(exception, (DeadlockError, ConnectionError)): return True elif isinstance(exception, ForeignKeyConstraintError): # FK errors can be retryable in case of race conditions return True elif isinstance(exception, RetryExhaustedException): return False elif isinstance(exception, (ConfigurationError, ValidationError)): return False else: # For unknown errors, be conservative and don't retry return False