Skip to main content

oxigdal_workflow/engine/
executor.rs

1//! Workflow execution engine.
2
3use crate::dag::{ResourcePool, TaskNode, WorkflowDag, create_execution_plan};
4use crate::engine::state::{
5    StatePersistence, TaskStatus, WorkflowCheckpoint, WorkflowState, WorkflowStatus,
6};
7use crate::error::{Result, WorkflowError};
8use async_trait::async_trait;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::Duration;
12use tokio::sync::{RwLock, Semaphore};
13use tokio::time::timeout;
14use tracing::{debug, error, info, warn};
15
16/// Task executor trait - implement this to define custom task execution logic.
17#[async_trait]
18pub trait TaskExecutor: Send + Sync {
19    /// Execute a task.
20    async fn execute(&self, task: &TaskNode, context: &ExecutionContext) -> Result<TaskOutput>;
21}
22
23/// Execution context provided to task executors.
24#[derive(Debug, Clone)]
25pub struct ExecutionContext {
26    /// Workflow execution ID.
27    pub execution_id: String,
28    /// Task ID.
29    pub task_id: String,
30    /// Shared workflow state.
31    pub state: Arc<RwLock<WorkflowState>>,
32    /// Input data from previous tasks.
33    pub inputs: std::collections::HashMap<String, serde_json::Value>,
34}
35
36/// Task execution output.
37#[derive(Debug, Clone)]
38pub struct TaskOutput {
39    /// Output data.
40    pub data: Option<serde_json::Value>,
41    /// Execution logs.
42    pub logs: Vec<String>,
43}
44
45/// Workflow executor configuration.
46#[derive(Debug, Clone)]
47pub struct ExecutorConfig {
48    /// Maximum concurrent tasks.
49    pub max_concurrent_tasks: usize,
50    /// Enable state persistence.
51    pub enable_persistence: bool,
52    /// State directory.
53    pub state_dir: String,
54    /// Resource pool.
55    pub resource_pool: ResourcePool,
56    /// Retry on failure.
57    pub retry_on_failure: bool,
58    /// Stop on first failure.
59    pub stop_on_failure: bool,
60    /// Checkpoint interval (save checkpoint every N tasks).
61    pub checkpoint_interval: usize,
62    /// Enable checkpoint-based recovery.
63    pub enable_checkpointing: bool,
64}
65
66impl Default for ExecutorConfig {
67    fn default() -> Self {
68        Self {
69            max_concurrent_tasks: 10,
70            enable_persistence: true,
71            state_dir: std::env::temp_dir()
72                .join("oxigdal-workflow")
73                .to_string_lossy()
74                .into_owned(),
75            resource_pool: ResourcePool::default(),
76            retry_on_failure: true,
77            stop_on_failure: false,
78            checkpoint_interval: 1, // Save after each task by default
79            enable_checkpointing: true,
80        }
81    }
82}
83
84/// Workflow executor.
85pub struct WorkflowExecutor<E: TaskExecutor> {
86    /// Configuration.
87    config: ExecutorConfig,
88    /// Task executor implementation.
89    task_executor: Arc<E>,
90    /// State persistence.
91    persistence: Option<StatePersistence>,
92    /// Semaphore for limiting concurrent tasks (reserved for future parallel execution).
93    _semaphore: Arc<Semaphore>,
94    /// Checkpoint sequence counter.
95    checkpoint_sequence: AtomicU64,
96    /// Tasks completed since last checkpoint.
97    tasks_since_checkpoint: AtomicU64,
98}
99
100impl<E: TaskExecutor> WorkflowExecutor<E> {
101    /// Create a new workflow executor.
102    pub fn new(config: ExecutorConfig, task_executor: E) -> Self {
103        let semaphore = Arc::new(Semaphore::new(config.max_concurrent_tasks));
104        let persistence = if config.enable_persistence {
105            Some(StatePersistence::new(config.state_dir.clone()))
106        } else {
107            None
108        };
109
110        Self {
111            config,
112            task_executor: Arc::new(task_executor),
113            persistence,
114            _semaphore: semaphore,
115            checkpoint_sequence: AtomicU64::new(0),
116            tasks_since_checkpoint: AtomicU64::new(0),
117        }
118    }
119
120    /// Save a checkpoint if conditions are met.
121    async fn maybe_save_checkpoint(&self, state: &WorkflowState, dag: &WorkflowDag) -> Result<()> {
122        if !self.config.enable_checkpointing {
123            return Ok(());
124        }
125
126        let persistence = match &self.persistence {
127            Some(p) => p,
128            None => return Ok(()),
129        };
130
131        let tasks_completed = self.tasks_since_checkpoint.fetch_add(1, Ordering::SeqCst) + 1;
132
133        if tasks_completed >= self.config.checkpoint_interval as u64 {
134            self.tasks_since_checkpoint.store(0, Ordering::SeqCst);
135            let seq = self.checkpoint_sequence.fetch_add(1, Ordering::SeqCst);
136
137            let checkpoint = WorkflowCheckpoint::new(state.clone(), dag.clone(), seq);
138            persistence.save_checkpoint(&checkpoint).await?;
139
140            debug!(
141                "Saved checkpoint {} for execution {}",
142                seq, state.execution_id
143            );
144        }
145
146        Ok(())
147    }
148
149    /// Force save a checkpoint immediately.
150    async fn save_checkpoint_now(&self, state: &WorkflowState, dag: &WorkflowDag) -> Result<()> {
151        if !self.config.enable_checkpointing {
152            return Ok(());
153        }
154
155        let persistence = match &self.persistence {
156            Some(p) => p,
157            None => return Ok(()),
158        };
159
160        self.tasks_since_checkpoint.store(0, Ordering::SeqCst);
161        let seq = self.checkpoint_sequence.fetch_add(1, Ordering::SeqCst);
162
163        let checkpoint = WorkflowCheckpoint::new(state.clone(), dag.clone(), seq);
164        persistence.save_checkpoint(&checkpoint).await?;
165
166        info!(
167            "Saved checkpoint {} for execution {}",
168            seq, state.execution_id
169        );
170        Ok(())
171    }
172
173    /// Execute a workflow.
174    pub async fn execute(
175        &self,
176        workflow_id: String,
177        execution_id: String,
178        dag: WorkflowDag,
179    ) -> Result<WorkflowState> {
180        info!(
181            "Starting workflow execution: workflow_id={}, execution_id={}",
182            workflow_id, execution_id
183        );
184
185        // Validate DAG
186        dag.validate()?;
187
188        // Create initial workflow state
189        let mut state = WorkflowState::new(workflow_id.clone(), execution_id.clone(), workflow_id);
190
191        // Initialize task states
192        for task in dag.tasks() {
193            state.init_task(task.id.clone());
194        }
195
196        state.start();
197
198        // Save initial state
199        if let Some(ref persistence) = self.persistence {
200            persistence.save(&state).await?;
201        }
202
203        // Save initial checkpoint with DAG
204        self.save_checkpoint_now(&state, &dag).await?;
205
206        let state_arc = Arc::new(RwLock::new(state));
207
208        // Create execution plan
209        let execution_plan = create_execution_plan(&dag)?;
210
211        info!(
212            "Execution plan created with {} levels",
213            execution_plan.len()
214        );
215
216        // Execute tasks level by level
217        for (level_idx, level) in execution_plan.iter().enumerate() {
218            info!("Executing level {} with {} tasks", level_idx, level.len());
219
220            let results = self.execute_level(&dag, &state_arc, level).await;
221
222            // Save checkpoint after each level
223            {
224                let state_guard = state_arc.read().await;
225                self.maybe_save_checkpoint(&state_guard, &dag).await?;
226            }
227
228            // Check for failures
229            let failed_tasks: Vec<_> = results
230                .iter()
231                .filter_map(|(task_id, result)| {
232                    if result.is_err() {
233                        Some(task_id.clone())
234                    } else {
235                        None
236                    }
237                })
238                .collect();
239
240            if !failed_tasks.is_empty() {
241                error!("Tasks failed: {:?}", failed_tasks);
242
243                if self.config.stop_on_failure {
244                    warn!("Stopping workflow execution due to failures");
245                    let mut state_guard = state_arc.write().await;
246                    state_guard.fail();
247
248                    if let Some(ref persistence) = self.persistence {
249                        persistence.save(&state_guard).await?;
250                    }
251
252                    // Save final checkpoint on failure
253                    self.save_checkpoint_now(&state_guard, &dag).await?;
254
255                    drop(state_guard);
256
257                    return Ok(Arc::try_unwrap(state_arc)
258                        .map(|rw| rw.into_inner())
259                        .unwrap_or_else(|arc| {
260                            tokio::task::block_in_place(|| arc.blocking_read().clone())
261                        }));
262                }
263            }
264        }
265
266        // Complete workflow
267        let mut state_guard = state_arc.write().await;
268
269        // Check if all tasks completed successfully
270        let all_completed = state_guard
271            .task_states
272            .values()
273            .all(|ts| ts.status == TaskStatus::Completed || ts.status == TaskStatus::Skipped);
274
275        if all_completed {
276            state_guard.complete();
277        } else {
278            state_guard.fail();
279        }
280
281        // Save final state
282        if let Some(ref persistence) = self.persistence {
283            persistence.save(&state_guard).await?;
284        }
285
286        // Save final checkpoint
287        self.save_checkpoint_now(&state_guard, &dag).await?;
288
289        info!(
290            "Workflow execution completed: status={:?}",
291            state_guard.status
292        );
293
294        drop(state_guard);
295
296        Ok(Arc::try_unwrap(state_arc)
297            .map(|rw| rw.into_inner())
298            .unwrap_or_else(|arc| tokio::task::block_in_place(|| arc.blocking_read().clone())))
299    }
300
301    /// Execute a level of tasks in parallel.
302    async fn execute_level(
303        &self,
304        dag: &WorkflowDag,
305        state: &Arc<RwLock<WorkflowState>>,
306        level: &[String],
307    ) -> Vec<(String, Result<()>)> {
308        let mut results = Vec::new();
309
310        for task_id in level {
311            let result = self
312                .execute_task(
313                    task_id,
314                    dag,
315                    state,
316                    &*self.task_executor,
317                    self.config.retry_on_failure,
318                )
319                .await;
320            results.push((task_id.clone(), result));
321        }
322
323        results
324    }
325
326    /// Execute a single task.
327    async fn execute_task(
328        &self,
329        task_id: &str,
330        dag: &WorkflowDag,
331        state: &Arc<RwLock<WorkflowState>>,
332        executor: &E,
333        retry_on_failure: bool,
334    ) -> Result<()> {
335        let task = dag
336            .get_task(task_id)
337            .ok_or_else(|| WorkflowError::not_found(format!("Task '{}'", task_id)))?;
338
339        debug!("Executing task: {}", task_id);
340
341        // Check dependencies
342        if !self.check_dependencies(task_id, dag, state).await? {
343            warn!("Skipping task {} due to failed dependencies", task_id);
344            let mut state_guard = state.write().await;
345            state_guard.skip_task(task_id)?;
346            return Ok(());
347        }
348
349        // Mark task as started
350        {
351            let mut state_guard = state.write().await;
352            state_guard.start_task(task_id)?;
353        }
354
355        // Execute with retry
356        let max_attempts = if retry_on_failure {
357            task.retry.max_attempts
358        } else {
359            1
360        };
361
362        let mut last_error = None;
363
364        for attempt in 0..max_attempts {
365            if attempt > 0 {
366                debug!("Retrying task {} (attempt {})", task_id, attempt + 1);
367
368                let delay_ms =
369                    task.retry.delay_ms as f64 * task.retry.backoff_multiplier.powi(attempt as i32);
370                let delay_ms = delay_ms.min(task.retry.max_delay_ms as f64) as u64;
371
372                tokio::time::sleep(Duration::from_millis(delay_ms)).await;
373            }
374
375            // Gather inputs from dependencies
376            let inputs = self.gather_inputs(task_id, dag, state).await?;
377
378            // Create execution context
379            let ctx = ExecutionContext {
380                execution_id: {
381                    let state_guard = state.read().await;
382                    state_guard.execution_id.clone()
383                },
384                task_id: task_id.to_string(),
385                state: Arc::clone(state),
386                inputs,
387            };
388
389            // Execute task with timeout
390            let task_timeout = Duration::from_secs(task.timeout_secs.unwrap_or(300));
391            let execute_result = timeout(task_timeout, executor.execute(task, &ctx)).await;
392
393            match execute_result {
394                Ok(Ok(output)) => {
395                    // Task succeeded
396                    let mut state_guard = state.write().await;
397                    state_guard.complete_task(task_id, output.data)?;
398
399                    for log in output.logs {
400                        state_guard.add_task_log(task_id, log)?;
401                    }
402
403                    info!("Task {} completed successfully", task_id);
404                    return Ok(());
405                }
406                Ok(Err(e)) => {
407                    warn!("Task {} failed: {}", task_id, e);
408                    last_error = Some(e);
409                }
410                Err(_) => {
411                    let timeout_error =
412                        WorkflowError::task_timeout(task_id, task_timeout.as_secs());
413                    warn!("Task {} timed out", task_id);
414                    last_error = Some(timeout_error);
415                }
416            }
417        }
418
419        // All attempts failed
420        let error = last_error.unwrap_or_else(|| WorkflowError::execution("Unknown error"));
421        let mut state_guard = state.write().await;
422        state_guard.fail_task(task_id, error.to_string())?;
423
424        error!("Task {} failed after {} attempts", task_id, max_attempts);
425        Err(error)
426    }
427
428    /// Check if all task dependencies are met.
429    async fn check_dependencies(
430        &self,
431        task_id: &str,
432        dag: &WorkflowDag,
433        state: &Arc<RwLock<WorkflowState>>,
434    ) -> Result<bool> {
435        let dependencies = dag.get_dependencies(task_id);
436        let state_guard = state.read().await;
437
438        for dep_id in dependencies {
439            if let Some(dep_state) = state_guard.get_task_state(&dep_id) {
440                if dep_state.status != TaskStatus::Completed {
441                    return Ok(false);
442                }
443            } else {
444                return Ok(false);
445            }
446        }
447
448        Ok(true)
449    }
450
451    /// Gather inputs from task dependencies.
452    async fn gather_inputs(
453        &self,
454        task_id: &str,
455        dag: &WorkflowDag,
456        state: &Arc<RwLock<WorkflowState>>,
457    ) -> Result<std::collections::HashMap<String, serde_json::Value>> {
458        let dependencies = dag.get_dependencies(task_id);
459        let state_guard = state.read().await;
460        let mut inputs = std::collections::HashMap::new();
461
462        for dep_id in dependencies {
463            if let Some(dep_state) = state_guard.get_task_state(&dep_id) {
464                if let Some(ref output) = dep_state.output {
465                    inputs.insert(dep_id.clone(), output.clone());
466                }
467            }
468        }
469
470        Ok(inputs)
471    }
472
473    /// Resume a workflow from a saved checkpoint.
474    ///
475    /// This method reconstructs the DAG from the checkpoint and continues
476    /// execution from where it left off, handling:
477    /// - Completed tasks (skipped)
478    /// - Interrupted tasks (reset to pending for retry)
479    /// - Failed tasks (depending on configuration)
480    /// - Pending tasks (executed normally)
481    pub async fn resume(&self, execution_id: String) -> Result<WorkflowState> {
482        let persistence = self
483            .persistence
484            .as_ref()
485            .ok_or_else(|| WorkflowError::state("Persistence is not enabled"))?;
486
487        // Try to load checkpoint first (includes DAG)
488        let mut checkpoint = persistence.load_checkpoint(&execution_id).await.map_err(|e| {
489            WorkflowError::state(format!(
490                "Failed to load checkpoint for recovery: {}. Ensure checkpointing was enabled during execution.",
491                e
492            ))
493        })?;
494
495        if checkpoint.state.is_terminal() {
496            return Err(WorkflowError::state("Cannot resume a terminal workflow"));
497        }
498
499        info!(
500            "Resuming workflow execution: execution_id={}, checkpoint_sequence={}",
501            execution_id, checkpoint.sequence
502        );
503
504        // Prepare checkpoint for resumption (reset interrupted tasks)
505        checkpoint.prepare_for_resume()?;
506
507        // Update checkpoint sequence for this executor
508        self.checkpoint_sequence
509            .store(checkpoint.sequence + 1, Ordering::SeqCst);
510
511        // Resume execution with recovered DAG and state
512        self.resume_from_checkpoint(checkpoint).await
513    }
514
515    /// Resume execution from a checkpoint.
516    async fn resume_from_checkpoint(
517        &self,
518        checkpoint: WorkflowCheckpoint,
519    ) -> Result<WorkflowState> {
520        let dag = checkpoint.dag.clone();
521
522        // Log recovery information (before moving state)
523        let completed = checkpoint.get_completed_tasks();
524        let pending = checkpoint.get_pending_tasks();
525        let interrupted = checkpoint.get_interrupted_tasks();
526        let failed = checkpoint.get_failed_tasks();
527
528        let mut state = checkpoint.state;
529
530        // Ensure workflow is in running state
531        if state.status != WorkflowStatus::Running {
532            state.status = WorkflowStatus::Running;
533        }
534
535        info!(
536            "Recovery state: {} completed, {} pending, {} interrupted, {} failed",
537            completed.len(),
538            pending.len(),
539            interrupted.len(),
540            failed.len()
541        );
542
543        // Save state update
544        if let Some(ref persistence) = self.persistence {
545            persistence.save(&state).await?;
546        }
547
548        let state_arc = Arc::new(RwLock::new(state));
549
550        // Create execution plan from DAG
551        let execution_plan = create_execution_plan(&dag)?;
552
553        info!("Resuming execution with {} levels", execution_plan.len());
554
555        // Execute tasks level by level, skipping completed ones
556        for (level_idx, level) in execution_plan.iter().enumerate() {
557            // Filter out already completed or skipped tasks
558            let tasks_to_execute: Vec<String> = {
559                let state_guard = state_arc.read().await;
560                level
561                    .iter()
562                    .filter(|task_id| {
563                        state_guard
564                            .get_task_state(task_id)
565                            .map(|ts| {
566                                !matches!(ts.status, TaskStatus::Completed | TaskStatus::Skipped)
567                            })
568                            .unwrap_or(true)
569                    })
570                    .cloned()
571                    .collect()
572            };
573
574            if tasks_to_execute.is_empty() {
575                debug!("Level {} has no tasks to execute, skipping", level_idx);
576                continue;
577            }
578
579            info!(
580                "Resuming level {} with {} tasks (skipping {} completed)",
581                level_idx,
582                tasks_to_execute.len(),
583                level.len() - tasks_to_execute.len()
584            );
585
586            let results = self
587                .execute_level(&dag, &state_arc, &tasks_to_execute)
588                .await;
589
590            // Save checkpoint after each level
591            {
592                let state_guard = state_arc.read().await;
593                self.maybe_save_checkpoint(&state_guard, &dag).await?;
594            }
595
596            // Check for failures
597            let failed_tasks: Vec<_> = results
598                .iter()
599                .filter_map(|(task_id, result)| {
600                    if result.is_err() {
601                        Some(task_id.clone())
602                    } else {
603                        None
604                    }
605                })
606                .collect();
607
608            if !failed_tasks.is_empty() {
609                error!("Tasks failed during resume: {:?}", failed_tasks);
610
611                if self.config.stop_on_failure {
612                    warn!("Stopping resumed workflow execution due to failures");
613                    let mut state_guard = state_arc.write().await;
614                    state_guard.fail();
615
616                    if let Some(ref persistence) = self.persistence {
617                        persistence.save(&state_guard).await?;
618                    }
619
620                    // Save final checkpoint on failure
621                    self.save_checkpoint_now(&state_guard, &dag).await?;
622
623                    drop(state_guard);
624
625                    return Ok(Arc::try_unwrap(state_arc)
626                        .map(|rw| rw.into_inner())
627                        .unwrap_or_else(|arc| {
628                            tokio::task::block_in_place(|| arc.blocking_read().clone())
629                        }));
630                }
631            }
632        }
633
634        // Complete workflow
635        let mut state_guard = state_arc.write().await;
636
637        // Check if all tasks completed successfully
638        let all_completed = state_guard
639            .task_states
640            .values()
641            .all(|ts| ts.status == TaskStatus::Completed || ts.status == TaskStatus::Skipped);
642
643        if all_completed {
644            state_guard.complete();
645        } else {
646            state_guard.fail();
647        }
648
649        // Save final state
650        if let Some(ref persistence) = self.persistence {
651            persistence.save(&state_guard).await?;
652        }
653
654        // Save final checkpoint
655        self.save_checkpoint_now(&state_guard, &dag).await?;
656
657        info!(
658            "Resumed workflow execution completed: status={:?}",
659            state_guard.status
660        );
661
662        drop(state_guard);
663
664        Ok(Arc::try_unwrap(state_arc)
665            .map(|rw| rw.into_inner())
666            .unwrap_or_else(|arc| tokio::task::block_in_place(|| arc.blocking_read().clone())))
667    }
668
669    /// Resume a workflow from a specific checkpoint sequence.
670    pub async fn resume_from_sequence(
671        &self,
672        execution_id: String,
673        sequence: u64,
674    ) -> Result<WorkflowState> {
675        let persistence = self
676            .persistence
677            .as_ref()
678            .ok_or_else(|| WorkflowError::state("Persistence is not enabled"))?;
679
680        let mut checkpoint = persistence
681            .load_checkpoint_by_sequence(&execution_id, sequence)
682            .await?;
683
684        if checkpoint.state.is_terminal() {
685            return Err(WorkflowError::state("Cannot resume a terminal workflow"));
686        }
687
688        info!(
689            "Resuming workflow from specific checkpoint: execution_id={}, sequence={}",
690            execution_id, sequence
691        );
692
693        // Prepare checkpoint for resumption
694        checkpoint.prepare_for_resume()?;
695
696        // Update checkpoint sequence
697        self.checkpoint_sequence
698            .store(sequence + 1, Ordering::SeqCst);
699
700        self.resume_from_checkpoint(checkpoint).await
701    }
702
703    /// Get recovery information for an execution.
704    pub async fn get_recovery_info(&self, execution_id: &str) -> Result<RecoveryInfo> {
705        let persistence = self
706            .persistence
707            .as_ref()
708            .ok_or_else(|| WorkflowError::state("Persistence is not enabled"))?;
709
710        let checkpoint = persistence.load_checkpoint(execution_id).await?;
711
712        Ok(RecoveryInfo {
713            execution_id: execution_id.to_string(),
714            checkpoint_sequence: checkpoint.sequence,
715            checkpoint_created_at: checkpoint.created_at,
716            workflow_status: checkpoint.state.status,
717            completed_tasks: checkpoint.get_completed_tasks(),
718            pending_tasks: checkpoint.get_pending_tasks(),
719            interrupted_tasks: checkpoint.get_interrupted_tasks(),
720            failed_tasks: checkpoint.get_failed_tasks(),
721            skipped_tasks: checkpoint.get_skipped_tasks(),
722            can_resume: !checkpoint.state.is_terminal(),
723        })
724    }
725
726    /// List available checkpoints for an execution.
727    pub async fn list_checkpoints(&self, execution_id: &str) -> Result<Vec<u64>> {
728        let persistence = self
729            .persistence
730            .as_ref()
731            .ok_or_else(|| WorkflowError::state("Persistence is not enabled"))?;
732
733        persistence.list_checkpoints(execution_id).await
734    }
735
736    /// Clean up old checkpoints, keeping only the latest N.
737    pub async fn cleanup_checkpoints(
738        &self,
739        execution_id: &str,
740        keep_count: usize,
741    ) -> Result<usize> {
742        let persistence = self
743            .persistence
744            .as_ref()
745            .ok_or_else(|| WorkflowError::state("Persistence is not enabled"))?;
746
747        let checkpoints = persistence.list_checkpoints(execution_id).await?;
748
749        if checkpoints.len() <= keep_count {
750            return Ok(0);
751        }
752
753        let to_delete = checkpoints.len() - keep_count;
754        let mut deleted = 0;
755
756        for seq in checkpoints.iter().take(to_delete) {
757            if persistence
758                .delete_checkpoint(execution_id, *seq)
759                .await
760                .is_ok()
761            {
762                deleted += 1;
763            }
764        }
765
766        Ok(deleted)
767    }
768}
769
770/// Information about workflow recovery state.
771#[derive(Debug, Clone)]
772pub struct RecoveryInfo {
773    /// Execution ID.
774    pub execution_id: String,
775    /// Latest checkpoint sequence number.
776    pub checkpoint_sequence: u64,
777    /// When the checkpoint was created.
778    pub checkpoint_created_at: chrono::DateTime<chrono::Utc>,
779    /// Current workflow status.
780    pub workflow_status: WorkflowStatus,
781    /// Tasks that completed successfully.
782    pub completed_tasks: Vec<String>,
783    /// Tasks that are pending execution.
784    pub pending_tasks: Vec<String>,
785    /// Tasks that were interrupted (running when checkpoint saved).
786    pub interrupted_tasks: Vec<String>,
787    /// Tasks that failed.
788    pub failed_tasks: Vec<String>,
789    /// Tasks that were skipped.
790    pub skipped_tasks: Vec<String>,
791    /// Whether the workflow can be resumed.
792    pub can_resume: bool,
793}
794
795#[cfg(test)]
796mod tests {
797    use super::*;
798    use crate::dag::graph::{ResourceRequirements, RetryPolicy};
799    use crate::engine::state::WorkflowStatus;
800    use std::collections::HashMap;
801
802    struct DummyExecutor;
803
804    #[async_trait]
805    impl TaskExecutor for DummyExecutor {
806        async fn execute(
807            &self,
808            _task: &TaskNode,
809            _context: &ExecutionContext,
810        ) -> Result<TaskOutput> {
811            Ok(TaskOutput {
812                data: Some(serde_json::json!({"result": "success"})),
813                logs: vec!["Task executed".to_string()],
814            })
815        }
816    }
817
818    fn create_test_task(id: &str) -> TaskNode {
819        TaskNode {
820            id: id.to_string(),
821            name: id.to_string(),
822            description: None,
823            config: serde_json::json!({}),
824            retry: RetryPolicy::default(),
825            timeout_secs: Some(60),
826            resources: ResourceRequirements::default(),
827            metadata: HashMap::new(),
828        }
829    }
830
831    #[tokio::test]
832    async fn test_simple_workflow() {
833        let mut dag = WorkflowDag::new();
834        dag.add_task(create_test_task("task1")).ok();
835
836        let executor = WorkflowExecutor::new(ExecutorConfig::default(), DummyExecutor);
837
838        let result = executor
839            .execute("wf1".to_string(), "exec1".to_string(), dag)
840            .await;
841
842        assert!(result.is_ok());
843        let state = result.expect("Expected workflow state");
844        assert_eq!(state.status, WorkflowStatus::Completed);
845    }
846}