1use 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#[async_trait]
18pub trait TaskExecutor: Send + Sync {
19 async fn execute(&self, task: &TaskNode, context: &ExecutionContext) -> Result<TaskOutput>;
21}
22
23#[derive(Debug, Clone)]
25pub struct ExecutionContext {
26 pub execution_id: String,
28 pub task_id: String,
30 pub state: Arc<RwLock<WorkflowState>>,
32 pub inputs: std::collections::HashMap<String, serde_json::Value>,
34}
35
36#[derive(Debug, Clone)]
38pub struct TaskOutput {
39 pub data: Option<serde_json::Value>,
41 pub logs: Vec<String>,
43}
44
45#[derive(Debug, Clone)]
47pub struct ExecutorConfig {
48 pub max_concurrent_tasks: usize,
50 pub enable_persistence: bool,
52 pub state_dir: String,
54 pub resource_pool: ResourcePool,
56 pub retry_on_failure: bool,
58 pub stop_on_failure: bool,
60 pub checkpoint_interval: usize,
62 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, enable_checkpointing: true,
80 }
81 }
82}
83
84pub struct WorkflowExecutor<E: TaskExecutor> {
86 config: ExecutorConfig,
88 task_executor: Arc<E>,
90 persistence: Option<StatePersistence>,
92 _semaphore: Arc<Semaphore>,
94 checkpoint_sequence: AtomicU64,
96 tasks_since_checkpoint: AtomicU64,
98}
99
100impl<E: TaskExecutor> WorkflowExecutor<E> {
101 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 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 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 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 dag.validate()?;
187
188 let mut state = WorkflowState::new(workflow_id.clone(), execution_id.clone(), workflow_id);
190
191 for task in dag.tasks() {
193 state.init_task(task.id.clone());
194 }
195
196 state.start();
197
198 if let Some(ref persistence) = self.persistence {
200 persistence.save(&state).await?;
201 }
202
203 self.save_checkpoint_now(&state, &dag).await?;
205
206 let state_arc = Arc::new(RwLock::new(state));
207
208 let execution_plan = create_execution_plan(&dag)?;
210
211 info!(
212 "Execution plan created with {} levels",
213 execution_plan.len()
214 );
215
216 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 {
224 let state_guard = state_arc.read().await;
225 self.maybe_save_checkpoint(&state_guard, &dag).await?;
226 }
227
228 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 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 let mut state_guard = state_arc.write().await;
268
269 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 if let Some(ref persistence) = self.persistence {
283 persistence.save(&state_guard).await?;
284 }
285
286 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 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 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 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 {
351 let mut state_guard = state.write().await;
352 state_guard.start_task(task_id)?;
353 }
354
355 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 let inputs = self.gather_inputs(task_id, dag, state).await?;
377
378 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 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 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 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 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 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 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 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 checkpoint.prepare_for_resume()?;
506
507 self.checkpoint_sequence
509 .store(checkpoint.sequence + 1, Ordering::SeqCst);
510
511 self.resume_from_checkpoint(checkpoint).await
513 }
514
515 async fn resume_from_checkpoint(
517 &self,
518 checkpoint: WorkflowCheckpoint,
519 ) -> Result<WorkflowState> {
520 let dag = checkpoint.dag.clone();
521
522 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 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 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 let execution_plan = create_execution_plan(&dag)?;
552
553 info!("Resuming execution with {} levels", execution_plan.len());
554
555 for (level_idx, level) in execution_plan.iter().enumerate() {
557 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 {
592 let state_guard = state_arc.read().await;
593 self.maybe_save_checkpoint(&state_guard, &dag).await?;
594 }
595
596 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 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 let mut state_guard = state_arc.write().await;
636
637 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 if let Some(ref persistence) = self.persistence {
651 persistence.save(&state_guard).await?;
652 }
653
654 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 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 checkpoint.prepare_for_resume()?;
695
696 self.checkpoint_sequence
698 .store(sequence + 1, Ordering::SeqCst);
699
700 self.resume_from_checkpoint(checkpoint).await
701 }
702
703 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 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 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#[derive(Debug, Clone)]
772pub struct RecoveryInfo {
773 pub execution_id: String,
775 pub checkpoint_sequence: u64,
777 pub checkpoint_created_at: chrono::DateTime<chrono::Utc>,
779 pub workflow_status: WorkflowStatus,
781 pub completed_tasks: Vec<String>,
783 pub pending_tasks: Vec<String>,
785 pub interrupted_tasks: Vec<String>,
787 pub failed_tasks: Vec<String>,
789 pub skipped_tasks: Vec<String>,
791 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}