1use crate::error::{Result, WorkflowError};
20use crate::task::{Task, TaskId, TaskResult, TaskState};
21use crate::workflow::{Workflow, WorkflowId, WorkflowState};
22use async_trait::async_trait;
23use dashmap::DashMap;
24use serde::{Deserialize, Serialize};
25use std::collections::{HashMap, HashSet};
26use std::sync::Arc;
27use std::time::{Duration, Instant};
28use tokio::sync::{mpsc, watch, RwLock, Semaphore};
29use tokio::time::timeout;
30use tracing::{debug, info, warn};
31
32#[async_trait]
34pub trait TaskExecutor: Send + Sync {
35 async fn execute(&self, task: &Task) -> Result<TaskResult>;
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ExecutionCheckpoint {
52 pub workflow_id: WorkflowId,
54 pub completed_task_ids: Vec<TaskId>,
56 pub variables: HashMap<String, serde_json::Value>,
58 pub timestamp_secs: u64,
60}
61
62impl ExecutionCheckpoint {
63 pub fn to_json(&self) -> Result<String> {
69 serde_json::to_string(self).map_err(WorkflowError::Serialization)
70 }
71
72 pub fn from_json(json: &str) -> Result<Self> {
78 serde_json::from_str(json).map_err(WorkflowError::Serialization)
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct ExecutionContext {
85 pub workflow_id: WorkflowId,
87 pub variables: Arc<DashMap<String, serde_json::Value>>,
89 pub results: Arc<DashMap<TaskId, TaskResult>>,
91}
92
93impl ExecutionContext {
94 #[must_use]
96 pub fn new(workflow_id: WorkflowId) -> Self {
97 Self {
98 workflow_id,
99 variables: Arc::new(DashMap::new()),
100 results: Arc::new(DashMap::new()),
101 }
102 }
103
104 #[must_use]
106 pub fn get_variable(&self, key: &str) -> Option<serde_json::Value> {
107 self.variables.get(key).map(|v| v.clone())
108 }
109
110 pub fn set_variable(&self, key: String, value: serde_json::Value) {
112 self.variables.insert(key, value);
113 }
114
115 #[must_use]
117 pub fn get_result(&self, task_id: &TaskId) -> Option<TaskResult> {
118 self.results.get(task_id).map(|r| r.clone())
119 }
120
121 pub fn store_result(&self, task_id: TaskId, result: TaskResult) {
123 self.results.insert(task_id, result);
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum WorkflowControl {
136 Pause,
141 Resume,
143 Cancel,
148}
149
150pub struct WorkflowExecutor {
152 task_executor: Arc<dyn TaskExecutor>,
154 max_concurrent: usize,
156 timeout: Option<Duration>,
158 pause_tx: watch::Sender<bool>,
161 pause_rx: watch::Receiver<bool>,
163 cancel_tx: watch::Sender<bool>,
166 cancel_rx: watch::Receiver<bool>,
168 resume_completed: HashSet<TaskId>,
170 resume_variables: HashMap<String, serde_json::Value>,
172}
173
174impl WorkflowExecutor {
175 #[must_use]
177 pub fn new(task_executor: Arc<dyn TaskExecutor>) -> Self {
178 let (pause_tx, pause_rx) = watch::channel(false);
179 let (cancel_tx, cancel_rx) = watch::channel(false);
180 Self {
181 task_executor,
182 max_concurrent: 4,
183 timeout: None,
184 pause_tx,
185 pause_rx,
186 cancel_tx,
187 cancel_rx,
188 resume_completed: HashSet::new(),
189 resume_variables: HashMap::new(),
190 }
191 }
192
193 #[must_use]
195 pub fn with_max_concurrent(mut self, max_concurrent: usize) -> Self {
196 self.max_concurrent = max_concurrent;
197 self
198 }
199
200 #[must_use]
202 pub fn with_timeout(mut self, timeout: Duration) -> Self {
203 self.timeout = Some(timeout);
204 self
205 }
206
207 pub fn pause(&self) {
212 if let Err(e) = self.pause_tx.send(true) {
213 warn!("Failed to send pause signal: {e}");
214 }
215 }
216
217 pub fn resume(&self) {
219 if let Err(e) = self.pause_tx.send(false) {
220 warn!("Failed to send resume signal: {e}");
221 }
222 }
223
224 pub fn send_control(&self, control: WorkflowControl) -> Result<()> {
236 match control {
237 WorkflowControl::Pause => {
238 self.pause_tx
239 .send(true)
240 .map_err(|e| WorkflowError::generic(format!("pause send failed: {e}")))?;
241 }
242 WorkflowControl::Resume => {
243 self.pause_tx
244 .send(false)
245 .map_err(|e| WorkflowError::generic(format!("resume send failed: {e}")))?;
246 }
247 WorkflowControl::Cancel => {
248 self.cancel_tx
249 .send(true)
250 .map_err(|e| WorkflowError::generic(format!("cancel send failed: {e}")))?;
251 let _ = self.pause_tx.send(true);
253 }
254 }
255 Ok(())
256 }
257
258 #[must_use]
264 pub fn is_paused(&self) -> bool {
265 *self.pause_rx.borrow()
266 }
267
268 pub fn resume_from_checkpoint(&mut self, checkpoint_json: &str) -> Result<()> {
275 let cp = ExecutionCheckpoint::from_json(checkpoint_json)?;
276 self.resume_completed = cp.completed_task_ids.into_iter().collect();
277 self.resume_variables = cp.variables;
278 info!(
279 "Loaded checkpoint with {} completed tasks",
280 self.resume_completed.len()
281 );
282 Ok(())
283 }
284
285 pub async fn execute(&self, workflow: &mut Workflow) -> Result<ExecutionResult> {
298 info!("Starting workflow execution: {}", workflow.name);
299
300 workflow.validate()?;
302
303 workflow.state = WorkflowState::Running;
305
306 let start_time = Instant::now();
307 let context = ExecutionContext::new(workflow.id);
308
309 for (key, value) in &workflow.config.variables {
311 context.set_variable(key.clone(), value.clone());
312 }
313
314 for (key, value) in &self.resume_variables {
316 context.set_variable(key.clone(), value.clone());
317 }
318
319 let task_order = workflow.topological_sort()?;
321
322 let completed_tasks: Arc<RwLock<HashSet<TaskId>>> =
324 Arc::new(RwLock::new(self.resume_completed.clone()));
325 let failed_tasks: Arc<RwLock<HashSet<TaskId>>> = Arc::new(RwLock::new(HashSet::new()));
326
327 let semaphore = Arc::new(Semaphore::new(
329 workflow
330 .config
331 .max_concurrent_tasks
332 .min(self.max_concurrent),
333 ));
334
335 let (tx, mut rx) = mpsc::channel(100);
337
338 let pause_rx = self.pause_rx.clone();
340 let cancel_rx = self.cancel_rx.clone();
342
343 let mut active_tasks = 0;
345 let mut task_iter = task_order.iter();
346
347 loop {
348 if let Some(timeout_duration) = self.timeout {
350 if start_time.elapsed() > timeout_duration {
351 workflow.state = WorkflowState::Failed;
352 return Err(WorkflowError::generic("Workflow execution timeout"));
353 }
354 }
355
356 if active_tasks == 0 && *cancel_rx.borrow() {
358 workflow.state = WorkflowState::Failed;
359 return Err(WorkflowError::generic("Workflow cancelled"));
360 }
361
362 if active_tasks == 0 && *pause_rx.borrow() {
364 let completed = completed_tasks.read().await;
367 let variables: HashMap<String, serde_json::Value> = context
368 .variables
369 .iter()
370 .map(|e| (e.key().clone(), e.value().clone()))
371 .collect();
372 let checkpoint = ExecutionCheckpoint {
373 workflow_id: workflow.id,
374 completed_task_ids: completed.iter().copied().collect(),
375 variables,
376 timestamp_secs: std::time::SystemTime::now()
377 .duration_since(std::time::UNIX_EPOCH)
378 .unwrap_or_default()
379 .as_secs(),
380 };
381 drop(completed);
382
383 let checkpoint_json = checkpoint.to_json()?;
384 info!(
385 "Workflow paused; checkpoint: {} bytes",
386 checkpoint_json.len()
387 );
388
389 workflow.state = WorkflowState::Paused;
390 return Ok(ExecutionResult {
391 workflow_id: workflow.id,
392 state: WorkflowState::Paused,
393 duration: start_time.elapsed(),
394 task_results: context
395 .results
396 .iter()
397 .map(|entry| (*entry.key(), entry.value().clone()))
398 .collect(),
399 checkpoint: Some(checkpoint_json),
400 });
401 }
402
403 while active_tasks < task_order.len() {
405 let Some(&task_id) = task_iter.next() else {
406 break;
407 };
408
409 let deps = workflow.get_dependencies(&task_id);
411 let completed = completed_tasks.read().await;
412 let failed = failed_tasks.read().await;
413
414 let already_done = completed.contains(&task_id);
416 let deps_satisfied = deps.iter().all(|dep| completed.contains(dep));
417 let deps_failed = deps.iter().any(|dep| failed.contains(dep));
418
419 drop(completed);
420 drop(failed);
421
422 if already_done {
424 active_tasks += 1;
425 let result = TaskResult {
427 task_id,
428 status: TaskState::Completed,
429 data: None,
430 error: None,
431 duration: Duration::ZERO,
432 outputs: Vec::new(),
433 };
434 let tx2 = tx.clone();
435 let completed2 = completed_tasks.clone();
436 tokio::spawn(async move {
437 completed2.write().await.insert(task_id);
438 let _ = tx2.send((task_id, result)).await;
439 });
440 continue;
441 }
442
443 if deps_failed {
444 if workflow.config.fail_fast {
445 workflow.state = WorkflowState::Failed;
446 return Err(WorkflowError::DependencyFailed(task_id.to_string()));
447 }
448 if let Some(task) = workflow.get_task_mut(&task_id) {
450 task.set_state(TaskState::Skipped)?;
451 }
452 continue;
453 }
454
455 if !deps_satisfied {
456 continue;
457 }
458
459 let Some(task) = workflow.get_task(&task_id).cloned() else {
461 continue;
462 };
463
464 if !self.should_run_task(&task, &context).await {
466 if let Some(t) = workflow.get_task_mut(&task_id) {
467 t.set_state(TaskState::Skipped)?;
468 }
469 completed_tasks.write().await.insert(task_id);
470 continue;
471 }
472
473 let executor = self.task_executor.clone();
475 let sem = semaphore.clone();
476 let ctx = context.clone();
477 let tx = tx.clone();
478 let completed = completed_tasks.clone();
479 let failed = failed_tasks.clone();
480
481 tokio::spawn(async move {
482 let Ok(_permit) = sem.acquire().await else {
483 let _ = tx
484 .send((
485 task_id,
486 TaskResult {
487 task_id,
488 status: TaskState::Failed,
489 data: None,
490 error: Some("Semaphore closed".to_string()),
491 duration: std::time::Duration::ZERO,
492 outputs: Vec::new(),
493 },
494 ))
495 .await;
496 return;
497 };
498 let result = Self::execute_task(executor, &task, &ctx).await;
499
500 let success = matches!(result.status, TaskState::Completed);
501 if success {
502 completed.write().await.insert(task_id);
503 } else {
504 failed.write().await.insert(task_id);
505 }
506
507 ctx.store_result(task_id, result.clone());
508 let _ = tx.send((task_id, result)).await;
509 });
510
511 active_tasks += 1;
512 }
513
514 if active_tasks == 0 {
516 break;
517 }
518
519 if let Some((_task_id, result)) = rx.recv().await {
520 active_tasks -= 1;
521
522 if !matches!(result.status, TaskState::Completed) && workflow.config.fail_fast {
523 workflow.state = WorkflowState::Failed;
524 return Err(WorkflowError::TaskExecutionFailed {
525 task_id: result.task_id.to_string(),
526 reason: result.error.unwrap_or_else(|| "Unknown error".to_string()),
527 });
528 }
529 } else {
530 break;
531 }
532 }
533
534 let failed = failed_tasks.read().await;
536 let final_state = if failed.is_empty() {
537 WorkflowState::Completed
538 } else {
539 WorkflowState::Failed
540 };
541
542 workflow.state = final_state;
543
544 info!(
545 "Workflow execution completed: {} in {:?}",
546 workflow.name,
547 start_time.elapsed()
548 );
549
550 Ok(ExecutionResult {
551 workflow_id: workflow.id,
552 state: final_state,
553 duration: start_time.elapsed(),
554 task_results: context
555 .results
556 .iter()
557 .map(|entry| (*entry.key(), entry.value().clone()))
558 .collect(),
559 checkpoint: None,
560 })
561 }
562
563 async fn execute_task(
564 executor: Arc<dyn TaskExecutor>,
565 task: &Task,
566 _context: &ExecutionContext,
567 ) -> TaskResult {
568 debug!("Executing task: {}", task.name);
569 let start = Instant::now();
570
571 let result = if let Some(task_timeout) = Some(task.timeout) {
572 match timeout(task_timeout, executor.execute(task)).await {
573 Ok(Ok(result)) => result,
574 Ok(Err(e)) => TaskResult {
575 task_id: task.id,
576 status: TaskState::Failed,
577 data: None,
578 error: Some(e.to_string()),
579 duration: start.elapsed(),
580 outputs: Vec::new(),
581 },
582 Err(_) => TaskResult {
583 task_id: task.id,
584 status: TaskState::Failed,
585 data: None,
586 error: Some("Task timeout".to_string()),
587 duration: start.elapsed(),
588 outputs: Vec::new(),
589 },
590 }
591 } else {
592 match executor.execute(task).await {
593 Ok(result) => result,
594 Err(e) => TaskResult {
595 task_id: task.id,
596 status: TaskState::Failed,
597 data: None,
598 error: Some(e.to_string()),
599 duration: start.elapsed(),
600 outputs: Vec::new(),
601 },
602 }
603 };
604
605 debug!(
606 "Task {} completed with status: {:?}",
607 task.name, result.status
608 );
609
610 result
611 }
612
613 async fn should_run_task(&self, task: &Task, context: &ExecutionContext) -> bool {
614 for condition in &task.conditions {
616 if !self.evaluate_condition(condition, context).await {
617 debug!("Task {} skipped due to condition: {}", task.name, condition);
618 return false;
619 }
620 }
621 true
622 }
623
624 async fn evaluate_condition(&self, condition: &str, context: &ExecutionContext) -> bool {
625 match parse_condition(condition, context) {
626 Ok(result) => result,
627 Err(err) => {
628 debug!(
629 "Condition parse error (treating as false): {} – {}",
630 condition, err
631 );
632 false
633 }
634 }
635 }
636}
637
638#[derive(Debug, Clone, PartialEq)]
642enum CondValue {
643 Int(i64),
645 Float(f64),
647 Str(String),
649 Bool(bool),
651}
652
653impl CondValue {
654 fn partial_cmp_values(&self, other: &Self) -> Option<std::cmp::Ordering> {
656 match (self, other) {
657 (Self::Int(a), Self::Int(b)) => a.partial_cmp(b),
658 (Self::Float(a), Self::Float(b)) => a.partial_cmp(b),
659 (Self::Int(a), Self::Float(b)) => (*a as f64).partial_cmp(b),
660 (Self::Float(a), Self::Int(b)) => a.partial_cmp(&(*b as f64)),
661 (Self::Str(a), Self::Str(b)) => a.partial_cmp(b),
662 _ => None,
663 }
664 }
665
666 fn eq_coerced(&self, other: &Self) -> bool {
668 match (self, other) {
669 (Self::Int(a), Self::Int(b)) => a == b,
670 (Self::Float(a), Self::Float(b)) => (a - b).abs() < f64::EPSILON,
671 (Self::Int(a), Self::Float(b)) => ((*a as f64) - b).abs() < f64::EPSILON,
672 (Self::Float(a), Self::Int(b)) => (a - (*b as f64)).abs() < f64::EPSILON,
673 (Self::Str(a), Self::Str(b)) => a.eq_ignore_ascii_case(b),
674 (Self::Bool(a), Self::Bool(b)) => a == b,
675 _ => false,
676 }
677 }
678}
679
680fn resolve_variable(path: &str, context: &ExecutionContext) -> Option<CondValue> {
687 let key = path.trim_start_matches("output.");
688 let json_val = context.get_variable(key)?;
689 json_to_cond_value(&json_val)
690}
691
692fn json_to_cond_value(v: &serde_json::Value) -> Option<CondValue> {
694 match v {
695 serde_json::Value::Bool(b) => Some(CondValue::Bool(*b)),
696 serde_json::Value::Number(n) => {
697 if let Some(i) = n.as_i64() {
698 Some(CondValue::Int(i))
699 } else {
700 n.as_f64().map(CondValue::Float)
701 }
702 }
703 serde_json::Value::String(s) => Some(CondValue::Str(s.clone())),
704 _ => None,
705 }
706}
707
708fn parse_literal(token: &str) -> Option<CondValue> {
716 let t = token.trim().trim_matches(|c| c == '"' || c == '\'');
717
718 match t.to_lowercase().as_str() {
720 "true" => return Some(CondValue::Bool(true)),
721 "false" => return Some(CondValue::Bool(false)),
722 _ => {}
723 }
724
725 let lower = t.to_lowercase();
727 let byte_units: &[(&str, i64)] = &[
728 ("tb", 1024 * 1024 * 1024 * 1024),
729 ("gb", 1024 * 1024 * 1024),
730 ("mb", 1024 * 1024),
731 ("kb", 1024),
732 ("b", 1),
733 ];
734 for &(suffix, multiplier) in byte_units {
735 if let Some(num_str) = lower.strip_suffix(suffix) {
736 let num_str = num_str.trim();
737 if let Ok(n) = num_str.parse::<f64>() {
738 return Some(CondValue::Int((n * multiplier as f64) as i64));
739 }
740 }
741 }
742
743 let duration_units: &[(&str, i64)] =
745 &[("ms", 1), ("s", 1_000), ("m", 60_000), ("h", 3_600_000)];
746 for &(suffix, multiplier_ms) in duration_units {
747 if let Some(num_str) = lower.strip_suffix(suffix) {
748 let num_str = num_str.trim();
749 if let Ok(n) = num_str.parse::<f64>() {
750 return Some(CondValue::Int((n * multiplier_ms as f64) as i64));
751 }
752 }
753 }
754
755 if let Ok(i) = t.parse::<i64>() {
757 return Some(CondValue::Int(i));
758 }
759
760 if let Ok(f) = t.parse::<f64>() {
762 return Some(CondValue::Float(f));
763 }
764
765 Some(CondValue::Str(t.to_string()))
767}
768
769pub fn parse_condition(
790 condition: &str,
791 context: &ExecutionContext,
792) -> std::result::Result<bool, String> {
793 let cond = condition.trim();
794
795 if let Some(rest) = cond.strip_prefix('!') {
797 return parse_condition(rest.trim(), context).map(|v| !v);
798 }
799
800 let operators = &[
803 ">=",
804 "<=",
805 "!=",
806 "==",
807 ">",
808 "<",
809 "contains",
810 "startswith",
811 "endswith",
812 ];
813
814 for &op in operators {
815 let split_pos = if op.chars().all(char::is_alphabetic) {
817 let lower = cond.to_lowercase();
819 let needle = format!(" {op} ");
820 lower
821 .find(&needle)
822 .map(|p| (p, p + needle.len() - 1, op.len()))
823 } else {
824 cond.find(op).map(|p| (p, p + op.len(), op.len()))
826 };
827
828 let Some((lhs_end, rhs_start, _)) = split_pos else {
829 continue;
830 };
831
832 let lhs_str = cond[..lhs_end].trim();
833 let rhs_str = cond[rhs_start..].trim();
834
835 let lhs = resolve_variable(lhs_str, context).or_else(|| parse_literal(lhs_str));
837
838 let rhs = resolve_variable(rhs_str, context).or_else(|| parse_literal(rhs_str));
840
841 let lhs = lhs.ok_or_else(|| format!("Cannot resolve LHS: {lhs_str}"))?;
842 let rhs = rhs.ok_or_else(|| format!("Cannot resolve RHS: {rhs_str}"))?;
843
844 let result = match op {
845 "==" => lhs.eq_coerced(&rhs),
846 "!=" => !lhs.eq_coerced(&rhs),
847 ">" => lhs
848 .partial_cmp_values(&rhs)
849 .is_some_and(|o| o == std::cmp::Ordering::Greater),
850 ">=" => lhs
851 .partial_cmp_values(&rhs)
852 .is_some_and(|o| o != std::cmp::Ordering::Less),
853 "<" => lhs
854 .partial_cmp_values(&rhs)
855 .is_some_and(|o| o == std::cmp::Ordering::Less),
856 "<=" => lhs
857 .partial_cmp_values(&rhs)
858 .is_some_and(|o| o != std::cmp::Ordering::Greater),
859 "contains" => {
860 if let (CondValue::Str(l), CondValue::Str(r)) = (&lhs, &rhs) {
861 l.to_lowercase().contains(&r.to_lowercase())
862 } else {
863 false
864 }
865 }
866 "startswith" => {
867 if let (CondValue::Str(l), CondValue::Str(r)) = (&lhs, &rhs) {
868 l.to_lowercase().starts_with(&r.to_lowercase())
869 } else {
870 false
871 }
872 }
873 "endswith" => {
874 if let (CondValue::Str(l), CondValue::Str(r)) = (&lhs, &rhs) {
875 l.to_lowercase().ends_with(&r.to_lowercase())
876 } else {
877 false
878 }
879 }
880 _ => false,
881 };
882
883 return Ok(result);
884 }
885
886 if let Some(val) = resolve_variable(cond, context) {
888 return Ok(match val {
889 CondValue::Bool(b) => b,
890 CondValue::Int(i) => i != 0,
891 CondValue::Float(f) => f != 0.0,
892 CondValue::Str(s) => !s.is_empty() && s.to_lowercase() != "false",
893 });
894 }
895
896 debug!("Condition not resolvable, defaulting to true: {}", cond);
898 Ok(true)
899}
900
901#[derive(Debug)]
903pub struct ExecutionResult {
904 pub workflow_id: WorkflowId,
906 pub state: WorkflowState,
908 pub duration: Duration,
910 pub task_results: HashMap<TaskId, TaskResult>,
912 pub checkpoint: Option<String>,
915}
916
917pub struct DefaultTaskExecutor;
919
920#[async_trait]
921impl TaskExecutor for DefaultTaskExecutor {
922 async fn execute(&self, task: &Task) -> Result<TaskResult> {
923 use crate::task::TaskType;
924
925 let start = Instant::now();
926
927 let result: Result<()> = match &task.task_type {
928 TaskType::Wait { duration } => {
929 tokio::time::sleep(*duration).await;
930 Ok(())
931 }
932 TaskType::HttpRequest {
933 url,
934 method,
935 headers: _,
936 body: _,
937 } => {
938 debug!("HTTP {:?} request to: {}", method, url);
939 info!("HTTP {} {}", format!("{:?}", method).to_uppercase(), url);
943 Ok(())
944 }
945 TaskType::Transcode {
946 input,
947 output,
948 preset,
949 params: _,
950 } => {
951 info!("Transcode: {:?} → {:?} (preset: {})", input, output, preset);
952 if !input.exists() {
957 return Err(WorkflowError::generic(format!(
958 "Transcode input not found: {}",
959 input.display()
960 )));
961 }
962 if let Some(parent) = output.parent() {
964 if !parent.as_os_str().is_empty() {
965 tokio::fs::create_dir_all(parent).await.map_err(|e| {
966 WorkflowError::generic(format!(
967 "Cannot create output directory {}: {e}",
968 parent.display()
969 ))
970 })?;
971 }
972 }
973 info!("Transcode task recorded for {:?}", output);
974 Ok(())
975 }
976 TaskType::QualityControl {
977 input,
978 profile,
979 rules,
980 } => {
981 info!(
982 "QualityControl: {:?} profile={} rules={:?}",
983 input, profile, rules
984 );
985 if !input.exists() {
986 return Err(WorkflowError::generic(format!(
987 "QC input not found: {}",
988 input.display()
989 )));
990 }
991 let metadata = tokio::fs::metadata(input)
994 .await
995 .map_err(|e| WorkflowError::generic(format!("QC metadata error: {e}")))?;
996 info!(
997 "QC target size: {} bytes, profile: {}",
998 metadata.len(),
999 profile
1000 );
1001 Ok(())
1002 }
1003 TaskType::Transfer {
1004 source,
1005 destination,
1006 protocol,
1007 options: _,
1008 } => {
1009 use crate::task::TransferProtocol;
1010 info!("Transfer: {} → {} via {:?}", source, destination, protocol);
1011 match protocol {
1015 TransferProtocol::Local => {
1016 let src_path = std::path::Path::new(source.as_str());
1017 let dst_path = std::path::Path::new(destination.as_str());
1018 if let Some(parent) = dst_path.parent() {
1019 if !parent.as_os_str().is_empty() {
1020 tokio::fs::create_dir_all(parent).await.map_err(|e| {
1021 WorkflowError::generic(format!(
1022 "Cannot create destination dir: {e}"
1023 ))
1024 })?;
1025 }
1026 }
1027 tokio::fs::copy(src_path, dst_path).await.map_err(|e| {
1028 WorkflowError::generic(format!(
1029 "Local copy {} → {} failed: {e}",
1030 src_path.display(),
1031 dst_path.display()
1032 ))
1033 })?;
1034 info!("Local transfer complete: {} → {}", source, destination);
1035 }
1036 other => {
1037 info!(
1038 "Remote transfer ({:?}) queued: {} → {}",
1039 other, source, destination
1040 );
1041 }
1042 }
1043 Ok(())
1044 }
1045 TaskType::Notification {
1046 channel,
1047 message,
1048 metadata: _,
1049 } => {
1050 use crate::task::NotificationChannel;
1051 match channel {
1052 NotificationChannel::Email { to, subject } => {
1053 info!(
1054 "Notification [Email] to={:?} subject={:?}: {}",
1055 to, subject, message
1056 );
1057 }
1058 NotificationChannel::Webhook { url } => {
1059 info!("Notification [Webhook] url={}: {}", url, message);
1060 }
1061 NotificationChannel::Slack {
1062 channel: slack_channel,
1063 webhook_url,
1064 } => {
1065 info!(
1066 "Notification [Slack] channel={} url={}: {}",
1067 slack_channel, webhook_url, message
1068 );
1069 }
1070 NotificationChannel::Discord { webhook_url } => {
1071 info!("Notification [Discord] url={}: {}", webhook_url, message);
1072 }
1073 }
1074 Ok(())
1075 }
1076 TaskType::CustomScript { script, args, env } => {
1077 info!(
1078 "CustomScript: {:?} args={:?} env_vars={}",
1079 script,
1080 args,
1081 env.len()
1082 );
1083 if !script.exists() {
1084 return Err(WorkflowError::generic(format!(
1085 "Script not found: {}",
1086 script.display()
1087 )));
1088 }
1089 let mut cmd = tokio::process::Command::new(script);
1091 cmd.args(args);
1092 for (k, v) in env {
1093 cmd.env(k, v);
1094 }
1095 let status = cmd.status().await.map_err(|e| {
1096 WorkflowError::generic(format!("Script {:?} failed to launch: {e}", script))
1097 })?;
1098 if !status.success() {
1099 return Err(WorkflowError::generic(format!(
1100 "Script {:?} exited with status: {}",
1101 script, status
1102 )));
1103 }
1104 info!("Script {:?} completed successfully", script);
1105 Ok(())
1106 }
1107 TaskType::Analysis {
1108 input,
1109 analyses,
1110 output,
1111 } => {
1112 info!(
1113 "Analysis: {:?} types={:?} output={:?}",
1114 input, analyses, output
1115 );
1116 if !input.exists() {
1117 return Err(WorkflowError::generic(format!(
1118 "Analysis input not found: {}",
1119 input.display()
1120 )));
1121 }
1122 if let Some(out_path) = output {
1124 if let Some(parent) = out_path.parent() {
1125 if !parent.as_os_str().is_empty() {
1126 tokio::fs::create_dir_all(parent).await.map_err(|e| {
1127 WorkflowError::generic(format!(
1128 "Cannot create analysis output dir: {e}"
1129 ))
1130 })?;
1131 }
1132 }
1133 }
1134 info!("Analysis task recorded for {:?}", input);
1138 Ok(())
1139 }
1140 TaskType::Conditional {
1141 condition,
1142 true_task,
1143 false_task,
1144 } => {
1145 let condition_result = match condition.trim().to_lowercase().as_str() {
1148 "true" | "1" | "yes" => true,
1149 "false" | "0" | "no" => false,
1150 other => {
1151 debug!(
1152 "Condition not resolvable as literal, defaulting to true: {}",
1153 other
1154 );
1155 true
1156 }
1157 };
1158
1159 let branch_task = if condition_result {
1160 true_task.as_deref()
1161 } else {
1162 false_task.as_deref()
1163 };
1164
1165 if let Some(inner_task) = branch_task {
1166 info!(
1167 "Conditional branch selected: condition={} task={}",
1168 condition_result, inner_task.name
1169 );
1170 let branch_result = self.execute(inner_task).await?;
1172 if !matches!(branch_result.status, TaskState::Completed) {
1173 return Err(WorkflowError::generic(format!(
1174 "Conditional branch task '{}' failed: {}",
1175 inner_task.name,
1176 branch_result.error.as_deref().unwrap_or("unknown")
1177 )));
1178 }
1179 } else {
1180 debug!("Conditional task: selected branch has no task, skipping");
1181 }
1182 Ok(())
1183 }
1184 };
1185
1186 match result {
1187 Ok(()) => Ok(TaskResult {
1188 task_id: task.id,
1189 status: TaskState::Completed,
1190 data: None,
1191 error: None,
1192 duration: start.elapsed(),
1193 outputs: Vec::new(),
1194 }),
1195 Err(e) => Ok(TaskResult {
1196 task_id: task.id,
1197 status: TaskState::Failed,
1198 data: None,
1199 error: Some(e.to_string()),
1200 duration: start.elapsed(),
1201 outputs: Vec::new(),
1202 }),
1203 }
1204 }
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use super::*;
1210 use crate::task::{Task, TaskType};
1211
1212 #[test]
1213 fn test_execution_context_creation() {
1214 let workflow_id = WorkflowId::new();
1215 let ctx = ExecutionContext::new(workflow_id);
1216 assert_eq!(ctx.workflow_id, workflow_id);
1217 }
1218
1219 #[test]
1220 fn test_execution_context_variables() {
1221 let ctx = ExecutionContext::new(WorkflowId::new());
1222 ctx.set_variable("key".to_string(), serde_json::json!("value"));
1223 assert_eq!(ctx.get_variable("key"), Some(serde_json::json!("value")));
1224 }
1225
1226 #[test]
1227 fn test_execution_context_results() {
1228 let ctx = ExecutionContext::new(WorkflowId::new());
1229 let task_id = TaskId::new();
1230 let result = TaskResult {
1231 task_id,
1232 status: TaskState::Completed,
1233 data: None,
1234 error: None,
1235 duration: Duration::from_secs(1),
1236 outputs: Vec::new(),
1237 };
1238
1239 ctx.store_result(task_id, result.clone());
1240 let retrieved = ctx.get_result(&task_id);
1241 assert!(retrieved.is_some());
1242 }
1243
1244 #[tokio::test]
1245 async fn test_default_executor_wait_task() {
1246 let executor = DefaultTaskExecutor;
1247 let task = Task::new(
1248 "wait-task",
1249 TaskType::Wait {
1250 duration: Duration::from_millis(10),
1251 },
1252 );
1253
1254 let result = executor
1255 .execute(&task)
1256 .await
1257 .expect("should succeed in test");
1258 assert_eq!(result.status, TaskState::Completed);
1259 }
1260
1261 #[tokio::test]
1262 async fn test_workflow_executor_creation() {
1263 let executor = WorkflowExecutor::new(Arc::new(DefaultTaskExecutor))
1264 .with_max_concurrent(2)
1265 .with_timeout(Duration::from_secs(60));
1266
1267 assert_eq!(executor.max_concurrent, 2);
1268 assert!(executor.timeout.is_some());
1269 }
1270
1271 #[tokio::test]
1272 async fn test_simple_workflow_execution() {
1273 let mut workflow = Workflow::new("test-workflow");
1274 let task = Task::new(
1275 "test-task",
1276 TaskType::Wait {
1277 duration: Duration::from_millis(10),
1278 },
1279 );
1280 workflow.add_task(task);
1281
1282 let executor = WorkflowExecutor::new(Arc::new(DefaultTaskExecutor));
1283 let result = executor
1284 .execute(&mut workflow)
1285 .await
1286 .expect("should succeed in test");
1287
1288 assert_eq!(result.state, WorkflowState::Completed);
1289 assert_eq!(result.task_results.len(), 1);
1290 }
1291}