"""Background task executor - Phase 10.4""" import asyncio from collections.abc import Callable, Coroutine from datetime import datetime from typing import Any from .manager import ( BackgroundTask, BackgroundTaskManager, BackgroundTaskStatus, get_background_task_manager, ) class BackgroundExecutor: """Executes background tasks with error handling and result storage. Provides methods to execute tasks synchronously or asynchronously, with full integration into BackgroundTaskManager for tracking. """ def __init__(self, task_manager: BackgroundTaskManager | None = None): """Initialize the executor. Args: task_manager: Optional BackgroundTaskManager instance. If not provided, uses the global singleton. """ self._task_manager = task_manager or get_background_task_manager() self._executors: dict[str, asyncio.Task] = {} async def execute_task( self, task_id: str, func: Callable[..., Coroutine[Any, Any, Any]], *args: Any, **kwargs: Any, ) -> BackgroundTask: """Execute a specific task by ID. Args: task_id: Unique task identifier func: Async function to execute *args: Positional arguments for the function **kwargs: Keyword arguments for the function Returns: The BackgroundTask with result or error """ # Get or create task record task = self._task_manager.get_task_status(task_id) if task is None: # Create a new task record if one doesn't exist task = BackgroundTask( id=task_id, name=f"executor_task_{task_id}", status=BackgroundTaskStatus.PENDING, created_at=datetime.now(), ) self._task_manager._tasks[task_id] = task # Update status to running task.status = BackgroundTaskStatus.RUNNING task.started_at = datetime.now() try: # Execute the async function result = await func(*args, **kwargs) task.status = BackgroundTaskStatus.COMPLETED task.result = result except Exception as e: task.status = BackgroundTaskStatus.FAILED task.error = f"{type(e).__name__}: {str(e)}" task.result = None finally: task.completed_at = datetime.now() # Clean up executor reference if task_id in self._executors: del self._executors[task_id] return task async def execute_async( self, task_id: str, func: Callable[..., Coroutine[Any, Any, Any]], *args: Any, **kwargs: Any, ) -> str: """Execute a task asynchronously in the background. Args: task_id: Unique task identifier func: Async function to execute *args: Positional arguments for the function **kwargs: Keyword arguments for the function Returns: The task ID """ # Create task record if it doesn't exist if self._task_manager.get_task_status(task_id) is None: self._task_manager._tasks[task_id] = BackgroundTask( id=task_id, name=f"async_task_{task_id}", status=BackgroundTaskStatus.PENDING, created_at=datetime.now(), ) # Create and store the asyncio task async_task = asyncio.create_task(self.execute_task(task_id, func, *args, **kwargs)) self._executors[task_id] = async_task return task_id def cancel_task(self, task_id: str) -> bool: """Cancel a running task. Args: task_id: The task ID to cancel Returns: True if cancelled, False if not found or not running """ if task_id not in self._executors: return False self._executors[task_id].cancel() del self._executors[task_id] # Update task status task = self._task_manager.get_task_status(task_id) if task: task.status = BackgroundTaskStatus.CANCELLED task.completed_at = datetime.now() return True return False def get_task_result(self, task_id: str) -> Any: """Get the result of a completed task. Args: task_id: The task ID Returns: The task result or None if not found/not completed """ task = self._task_manager.get_task_status(task_id) if task and task.status == BackgroundTaskStatus.COMPLETED: return task.result return None def get_task_error(self, task_id: str) -> str | None: """Get the error of a failed task. Args: task_id: The task ID Returns: The error message or None if not found/not failed """ task = self._task_manager.get_task_status(task_id) if task and task.status == BackgroundTaskStatus.FAILED: return task.error return None def is_task_running(self, task_id: str) -> bool: """Check if a task is currently running. Args: task_id: The task ID Returns: True if running, False otherwise """ return task_id in self._executors def wait_for_task(self, task_id: str, timeout: float | None = None) -> BackgroundTask: """Wait for a task to complete. Args: task_id: The task ID to wait for timeout: Optional timeout in seconds Returns: The completed BackgroundTask Raises: asyncio.TimeoutError: If task doesn't complete within timeout asyncio.CancelledError: If task is cancelled """ if task_id not in self._executors: task = self._task_manager.get_task_status(task_id) if task: return task raise ValueError(f"Task {task_id} not found") async def wait_task() -> BackgroundTask: await self._executors[task_id] return self._task_manager.get_task_status(task_id) return asyncio.run_until_complete(asyncio.wait_for(wait_task(), timeout=timeout)) @property def task_manager(self) -> BackgroundTaskManager: """Get the underlying task manager.""" return self._task_manager # Global executor instance _executor: BackgroundExecutor | None = None def get_background_executor() -> BackgroundExecutor: """Get the global BackgroundExecutor instance.""" global _executor if _executor is None: _executor = BackgroundExecutor() return _executor