Spaces:
Running
Running
| """ | |
| API Timeout Configuration for F1 Commentary Robot. | |
| This module centralizes all API timeout settings to ensure consistent | |
| timeout enforcement across the system. | |
| Validates: Requirement 10.5 | |
| """ | |
| import logging | |
| from typing import Optional, Callable, Any | |
| import functools | |
| import signal | |
| logger = logging.getLogger(__name__) | |
| # ============================================================================ | |
| # Timeout Constants (per Requirement 10.5) | |
| # ============================================================================ | |
| OPENF1_API_TIMEOUT = 5.0 # seconds | |
| ELEVENLABS_API_TIMEOUT = 3.0 # seconds | |
| AI_API_TIMEOUT = 1.5 # seconds | |
| # ============================================================================ | |
| # Timeout Enforcement Utilities | |
| # ============================================================================ | |
| class TimeoutError(Exception): | |
| """Exception raised when an operation times out.""" | |
| pass | |
| def timeout_handler(signum, frame): | |
| """Signal handler for timeout.""" | |
| raise TimeoutError("Operation timed out") | |
| def with_timeout(timeout_seconds: float): | |
| """ | |
| Decorator to enforce timeout on a function using signals. | |
| Note: This only works on Unix-like systems and only in the main thread. | |
| For cross-platform and thread-safe timeouts, use the timeout parameter | |
| in the respective API client libraries. | |
| Args: | |
| timeout_seconds: Maximum execution time in seconds | |
| Returns: | |
| Decorated function with timeout enforcement | |
| Example: | |
| @with_timeout(5.0) | |
| def slow_operation(): | |
| # ... implementation | |
| pass | |
| """ | |
| def decorator(func: Callable) -> Callable: | |
| def wrapper(*args, **kwargs): | |
| # Set up signal handler | |
| old_handler = signal.signal(signal.SIGALRM, timeout_handler) | |
| signal.alarm(int(timeout_seconds)) | |
| try: | |
| result = func(*args, **kwargs) | |
| finally: | |
| # Restore old handler and cancel alarm | |
| signal.alarm(0) | |
| signal.signal(signal.SIGALRM, old_handler) | |
| return result | |
| return wrapper | |
| return decorator | |
| def enforce_timeout(operation: Callable, timeout_seconds: float, | |
| *args, **kwargs) -> tuple[bool, Any]: | |
| """ | |
| Execute an operation with timeout enforcement. | |
| This is a functional approach to timeout enforcement that doesn't | |
| require decorators. Returns a tuple indicating success/failure. | |
| Args: | |
| operation: Callable to execute | |
| timeout_seconds: Maximum execution time in seconds | |
| *args: Positional arguments for operation | |
| **kwargs: Keyword arguments for operation | |
| Returns: | |
| Tuple of (success: bool, result: Any) | |
| If timeout occurs, returns (False, None) | |
| Example: | |
| success, result = enforce_timeout( | |
| api_client.fetch_data, | |
| 5.0, | |
| endpoint="/data" | |
| ) | |
| if not success: | |
| # Handle timeout | |
| pass | |
| """ | |
| # Set up signal handler | |
| old_handler = signal.signal(signal.SIGALRM, timeout_handler) | |
| signal.alarm(int(timeout_seconds)) | |
| try: | |
| result = operation(*args, **kwargs) | |
| signal.alarm(0) | |
| signal.signal(signal.SIGALRM, old_handler) | |
| return True, result | |
| except TimeoutError: | |
| logger.warning(f"Operation {operation.__name__} timed out after {timeout_seconds}s") | |
| signal.alarm(0) | |
| signal.signal(signal.SIGALRM, old_handler) | |
| return False, None | |
| except Exception as e: | |
| logger.error(f"Operation {operation.__name__} failed: {e}", exc_info=True) | |
| signal.alarm(0) | |
| signal.signal(signal.SIGALRM, old_handler) | |
| return False, None | |
| # ============================================================================ | |
| # Timeout Monitoring | |
| # ============================================================================ | |
| class TimeoutMonitor: | |
| """ | |
| Monitors API call timeouts and tracks timeout statistics. | |
| Helps identify APIs that frequently timeout and may need | |
| configuration adjustments or alternative approaches. | |
| """ | |
| def __init__(self): | |
| """Initialize timeout monitor.""" | |
| self._timeout_counts = {} | |
| self._total_calls = {} | |
| def record_timeout(self, api_name: str) -> None: | |
| """ | |
| Record a timeout for an API. | |
| Args: | |
| api_name: Name of the API that timed out | |
| """ | |
| self._timeout_counts[api_name] = self._timeout_counts.get(api_name, 0) + 1 | |
| self._total_calls[api_name] = self._total_calls.get(api_name, 0) + 1 | |
| # Log warning if timeout rate is high | |
| timeout_rate = self.get_timeout_rate(api_name) | |
| if timeout_rate > 0.3: # More than 30% timeouts | |
| logger.warning( | |
| f"[TimeoutMonitor] API {api_name} has high timeout rate: " | |
| f"{timeout_rate:.1%} ({self._timeout_counts[api_name]} timeouts)" | |
| ) | |
| def record_success(self, api_name: str) -> None: | |
| """ | |
| Record a successful API call (no timeout). | |
| Args: | |
| api_name: Name of the API | |
| """ | |
| self._total_calls[api_name] = self._total_calls.get(api_name, 0) + 1 | |
| def get_timeout_rate(self, api_name: str) -> float: | |
| """ | |
| Get timeout rate for an API. | |
| Args: | |
| api_name: Name of the API | |
| Returns: | |
| Timeout rate from 0.0 to 1.0 | |
| """ | |
| total = self._total_calls.get(api_name, 0) | |
| if total == 0: | |
| return 0.0 | |
| timeouts = self._timeout_counts.get(api_name, 0) | |
| return timeouts / total | |
| def get_timeout_stats(self) -> dict: | |
| """ | |
| Get timeout statistics for all APIs. | |
| Returns: | |
| Dictionary mapping API names to timeout statistics | |
| """ | |
| return { | |
| api: { | |
| "total_calls": self._total_calls.get(api, 0), | |
| "timeouts": self._timeout_counts.get(api, 0), | |
| "timeout_rate": self.get_timeout_rate(api) | |
| } | |
| for api in self._total_calls.keys() | |
| } | |
| def reset_stats(self, api_name: Optional[str] = None) -> None: | |
| """ | |
| Reset statistics for an API or all APIs. | |
| Args: | |
| api_name: API to reset, or None to reset all | |
| """ | |
| if api_name: | |
| self._timeout_counts.pop(api_name, None) | |
| self._total_calls.pop(api_name, None) | |
| else: | |
| self._timeout_counts.clear() | |
| self._total_calls.clear() | |
| # Global timeout monitor instance | |
| timeout_monitor = TimeoutMonitor() | |