"""Composite executor for sequential task execution.""" import time from typing import Any from app.executors.base import BaseExecutor, ExecutionResult class CompositeExecutor(BaseExecutor): """Execute a sequence of tasks in order. Executes each task in the sequence using the appropriate executor. Stops on first failure and returns partial results. Security measures: - Each sub-task uses the same validated executors - Sequential execution only (no parallelism) - Stops immediately on first failure Payload: { "steps": [ {"type": "ENV_UPDATE", "payload": {...}}, {"type": "DOCKER_RELOAD", "payload": {...}} ] } Result (success): { "steps": [ {"index": 0, "type": "ENV_UPDATE", "status": "completed", "result": {...}}, {"index": 1, "type": "DOCKER_RELOAD", "status": "completed", "result": {...}} ] } Result (failure at step 1): ExecutionResult.success = False ExecutionResult.error = "Step 1 (DOCKER_RELOAD) failed: " ExecutionResult.data = { "steps": [ {"index": 0, "type": "ENV_UPDATE", "status": "completed", "result": {...}}, {"index": 1, "type": "DOCKER_RELOAD", "status": "failed", "error": "..."} ] } """ @property def task_type(self) -> str: return "COMPOSITE" async def execute(self, payload: dict[str, Any]) -> ExecutionResult: """Execute a sequence of tasks. Args: payload: Must contain "steps" list of step definitions Returns: ExecutionResult with execution summary """ self.validate_payload(payload, ["steps"]) steps = payload["steps"] # Validate steps is a non-empty list if not isinstance(steps, list): return ExecutionResult( success=False, data={"steps": []}, error="'steps' must be a list of step definitions", ) if not steps: return ExecutionResult( success=False, data={"steps": []}, error="'steps' cannot be empty", ) # Import registry here to avoid circular imports from app.executors import get_executor self.logger.info( "composite_starting", total_steps=len(steps), step_types=[step.get("type", "UNKNOWN") if isinstance(step, dict) else "INVALID" for step in steps], ) start_time = time.time() results: list[dict[str, Any]] = [] for i, step in enumerate(steps): # Validate step structure if not isinstance(step, dict): self.logger.error("composite_invalid_step", step_index=i) return ExecutionResult( success=False, data={"steps": results}, error=f"Step {i} is not a valid step definition (must be dict)", ) step_type = step.get("type") step_payload = step.get("payload", {}) if not step_type: self.logger.error("composite_missing_type", step_index=i) return ExecutionResult( success=False, data={"steps": results}, error=f"Step {i} missing 'type' field", ) self.logger.info( "composite_step_starting", step_index=i, step_type=step_type, ) # Get executor for this step type try: executor = get_executor(step_type) except ValueError as e: self.logger.error( "composite_unknown_type", step_index=i, step_type=step_type, error=str(e), ) return ExecutionResult( success=False, data={"steps": results}, error=f"Step {i} ({step_type}) failed: {e}", ) # Execute the step try: result = await executor.execute(step_payload) step_result: dict[str, Any] = { "index": i, "type": step_type, "status": "completed" if result.success else "failed", "result": result.data, } if result.error: step_result["error"] = result.error results.append(step_result) self.logger.info( "composite_step_completed", step_index=i, step_type=step_type, success=result.success, ) # Stop on first failure if not result.success: duration_ms = (time.time() - start_time) * 1000 self.logger.warning( "composite_step_failed", step_index=i, step_type=step_type, error=result.error, ) return ExecutionResult( success=False, data={"steps": results}, error=f"Step {i} ({step_type}) failed: {result.error}", duration_ms=duration_ms, ) except Exception as e: duration_ms = (time.time() - start_time) * 1000 self.logger.error( "composite_step_exception", step_index=i, step_type=step_type, error=str(e), ) # Add failed step to results results.append({ "index": i, "type": step_type, "status": "failed", "error": str(e), }) return ExecutionResult( success=False, data={"steps": results}, error=f"Step {i} ({step_type}) failed: {e}", duration_ms=duration_ms, ) # All steps completed successfully duration_ms = (time.time() - start_time) * 1000 self.logger.info( "composite_completed", steps_completed=len(results), duration_ms=duration_ms, ) return ExecutionResult( success=True, data={"steps": results}, duration_ms=duration_ms, )