1use crate::error::{Result, WorkflowError};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::time::Duration;
8use uuid::Uuid;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct TaskId(Uuid);
13
14impl TaskId {
15 #[must_use]
17 pub fn new() -> Self {
18 Self(Uuid::new_v4())
19 }
20
21 #[must_use]
23 pub const fn as_uuid(&self) -> &Uuid {
24 &self.0
25 }
26}
27
28impl Default for TaskId {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl std::fmt::Display for TaskId {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(f, "{}", self.0)
37 }
38}
39
40impl From<Uuid> for TaskId {
41 fn from(uuid: Uuid) -> Self {
42 Self(uuid)
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "lowercase")]
49#[derive(Default)]
50pub enum TaskState {
51 #[default]
53 Pending,
54 Queued,
56 Running,
58 Completed,
60 Failed,
62 Cancelled,
64 Waiting,
66 Retrying,
68 Skipped,
70}
71
72impl TaskState {
73 #[must_use]
75 pub const fn is_terminal(&self) -> bool {
76 matches!(
77 self,
78 Self::Completed | Self::Failed | Self::Cancelled | Self::Skipped
79 )
80 }
81
82 #[must_use]
84 pub const fn is_active(&self) -> bool {
85 matches!(self, Self::Running | Self::Retrying)
86 }
87
88 #[must_use]
90 pub const fn can_start(&self) -> bool {
91 matches!(self, Self::Pending | Self::Queued)
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
97pub enum TaskPriority {
98 Low = 0,
100 #[default]
102 Normal = 1,
103 High = 2,
105 Critical = 3,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct RetryPolicy {
112 pub max_attempts: u32,
114 pub initial_delay: Duration,
116 pub max_delay: Duration,
118 pub backoff_multiplier: f64,
120 pub exponential_backoff: bool,
122}
123
124impl Default for RetryPolicy {
125 fn default() -> Self {
126 Self {
127 max_attempts: 3,
128 initial_delay: Duration::from_secs(1),
129 max_delay: Duration::from_secs(60),
130 backoff_multiplier: 2.0,
131 exponential_backoff: true,
132 }
133 }
134}
135
136impl RetryPolicy {
137 #[must_use]
139 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
140 if !self.exponential_backoff {
141 return self.initial_delay;
142 }
143
144 let delay = self.initial_delay.as_secs_f64()
145 * self
146 .backoff_multiplier
147 .powi(i32::try_from(attempt).unwrap_or(10));
148 let delay = delay.min(self.max_delay.as_secs_f64());
149 Duration::from_secs_f64(delay)
150 }
151
152 #[must_use]
154 pub const fn should_retry(&self, attempt: u32) -> bool {
155 attempt < self.max_attempts
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(tag = "type", rename_all = "snake_case")]
162pub enum TaskType {
163 Transcode {
165 input: PathBuf,
167 output: PathBuf,
169 preset: String,
171 #[serde(default)]
173 params: HashMap<String, serde_json::Value>,
174 },
175
176 QualityControl {
178 input: PathBuf,
180 profile: String,
182 #[serde(default)]
184 rules: Vec<String>,
185 },
186
187 Transfer {
189 source: String,
191 destination: String,
193 protocol: TransferProtocol,
195 #[serde(default)]
197 options: HashMap<String, String>,
198 },
199
200 Notification {
202 channel: NotificationChannel,
204 message: String,
206 #[serde(default)]
208 metadata: HashMap<String, String>,
209 },
210
211 CustomScript {
213 script: PathBuf,
215 #[serde(default)]
217 args: Vec<String>,
218 #[serde(default)]
220 env: HashMap<String, String>,
221 },
222
223 Analysis {
225 input: PathBuf,
227 analyses: Vec<AnalysisType>,
229 output: Option<PathBuf>,
231 },
232
233 Conditional {
235 condition: String,
237 true_task: Option<Box<Task>>,
239 false_task: Option<Box<Task>>,
241 },
242
243 Wait {
245 duration: Duration,
247 },
248
249 HttpRequest {
251 url: String,
253 method: HttpMethod,
255 #[serde(default)]
257 headers: HashMap<String, String>,
258 body: Option<String>,
260 },
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265#[serde(rename_all = "lowercase")]
266pub enum TransferProtocol {
267 Local,
269 Ftp,
271 Sftp,
273 S3,
275 Http,
277 Rsync,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(rename_all = "lowercase")]
284pub enum NotificationChannel {
285 Email {
287 to: Vec<String>,
289 subject: String,
291 },
292 Webhook {
294 url: String,
296 },
297 Slack {
299 channel: String,
301 webhook_url: String,
303 },
304 Discord {
306 webhook_url: String,
308 },
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313#[serde(rename_all = "snake_case")]
314pub enum AnalysisType {
315 AudioLevels,
317 VideoQuality,
319 SceneDetection,
321 BlackFrames,
323 Silence,
325 Color,
327 Motion,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333#[serde(rename_all = "UPPERCASE")]
334pub enum HttpMethod {
335 Get,
337 Post,
339 Put,
341 Delete,
343 Patch,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct Task {
350 pub id: TaskId,
352 pub name: String,
354 pub task_type: TaskType,
356 #[serde(default)]
358 pub state: TaskState,
359 #[serde(default)]
361 pub priority: TaskPriority,
362 #[serde(default)]
364 pub retry: RetryPolicy,
365 #[serde(default = "default_timeout")]
367 pub timeout: Duration,
368 #[serde(default)]
370 pub dependencies: Vec<TaskId>,
371 #[serde(default)]
373 pub metadata: HashMap<String, String>,
374 #[serde(default)]
376 pub retry_count: u32,
377 #[serde(default)]
379 pub conditions: Vec<String>,
380}
381
382fn default_timeout() -> Duration {
383 Duration::from_secs(3600) }
385
386impl Task {
387 #[must_use]
389 pub fn new(name: impl Into<String>, task_type: TaskType) -> Self {
390 Self {
391 id: TaskId::new(),
392 name: name.into(),
393 task_type,
394 state: TaskState::Pending,
395 priority: TaskPriority::default(),
396 retry: RetryPolicy::default(),
397 timeout: default_timeout(),
398 dependencies: Vec::new(),
399 metadata: HashMap::new(),
400 retry_count: 0,
401 conditions: Vec::new(),
402 }
403 }
404
405 pub fn add_dependency(&mut self, task_id: TaskId) {
407 if !self.dependencies.contains(&task_id) {
408 self.dependencies.push(task_id);
409 }
410 }
411
412 #[must_use]
414 pub fn with_priority(mut self, priority: TaskPriority) -> Self {
415 self.priority = priority;
416 self
417 }
418
419 #[must_use]
421 pub fn with_retry(mut self, retry: RetryPolicy) -> Self {
422 self.retry = retry;
423 self
424 }
425
426 #[must_use]
428 pub fn with_timeout(mut self, timeout: Duration) -> Self {
429 self.timeout = timeout;
430 self
431 }
432
433 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
435 self.metadata.insert(key.into(), value.into());
436 self
437 }
438
439 pub fn with_condition(mut self, condition: impl Into<String>) -> Self {
441 self.conditions.push(condition.into());
442 self
443 }
444
445 #[must_use]
447 pub const fn can_execute(&self) -> bool {
448 self.state.can_start()
449 }
450
451 pub fn set_state(&mut self, state: TaskState) -> Result<()> {
453 match (&self.state, &state) {
455 (TaskState::Completed | TaskState::Failed | TaskState::Cancelled, _)
456 if !matches!(state, TaskState::Pending) =>
457 {
458 return Err(WorkflowError::InvalidStateTransition {
459 from: format!("{:?}", self.state),
460 to: format!("{state:?}"),
461 });
462 }
463 _ => {}
464 }
465
466 self.state = state;
467 Ok(())
468 }
469
470 pub fn increment_retry(&mut self) {
472 self.retry_count += 1;
473 }
474
475 #[must_use]
477 pub fn should_retry(&self) -> bool {
478 self.retry.should_retry(self.retry_count)
479 }
480
481 #[must_use]
483 pub fn retry_delay(&self) -> Duration {
484 self.retry.delay_for_attempt(self.retry_count)
485 }
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct TaskResult {
491 pub task_id: TaskId,
493 pub status: TaskState,
495 pub data: Option<serde_json::Value>,
497 pub error: Option<String>,
499 pub duration: Duration,
501 #[serde(default)]
503 pub outputs: Vec<PathBuf>,
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn test_task_id_creation() {
512 let id1 = TaskId::new();
513 let id2 = TaskId::new();
514 assert_ne!(id1, id2);
515 }
516
517 #[test]
518 fn test_task_state_terminal() {
519 assert!(TaskState::Completed.is_terminal());
520 assert!(TaskState::Failed.is_terminal());
521 assert!(!TaskState::Running.is_terminal());
522 }
523
524 #[test]
525 fn test_task_state_active() {
526 assert!(TaskState::Running.is_active());
527 assert!(!TaskState::Pending.is_active());
528 }
529
530 #[test]
531 fn test_retry_policy_delay() {
532 let policy = RetryPolicy::default();
533 let delay1 = policy.delay_for_attempt(0);
534 let delay2 = policy.delay_for_attempt(1);
535 assert!(delay2 > delay1);
536 }
537
538 #[test]
539 fn test_retry_policy_max_attempts() {
540 let policy = RetryPolicy {
541 max_attempts: 3,
542 ..Default::default()
543 };
544 assert!(policy.should_retry(0));
545 assert!(policy.should_retry(2));
546 assert!(!policy.should_retry(3));
547 }
548
549 #[test]
550 fn test_task_creation() {
551 let task = Task::new(
552 "test-task",
553 TaskType::Wait {
554 duration: Duration::from_secs(10),
555 },
556 );
557 assert_eq!(task.name, "test-task");
558 assert_eq!(task.state, TaskState::Pending);
559 }
560
561 #[test]
562 fn test_task_with_priority() {
563 let task = Task::new(
564 "test",
565 TaskType::Wait {
566 duration: Duration::from_secs(1),
567 },
568 )
569 .with_priority(TaskPriority::High);
570 assert_eq!(task.priority, TaskPriority::High);
571 }
572
573 #[test]
574 fn test_task_add_dependency() {
575 let mut task = Task::new(
576 "test",
577 TaskType::Wait {
578 duration: Duration::from_secs(1),
579 },
580 );
581 let dep_id = TaskId::new();
582 task.add_dependency(dep_id);
583 assert_eq!(task.dependencies.len(), 1);
584 assert_eq!(task.dependencies[0], dep_id);
585 }
586
587 #[test]
588 fn test_task_state_transition() {
589 let mut task = Task::new(
590 "test",
591 TaskType::Wait {
592 duration: Duration::from_secs(1),
593 },
594 );
595 assert!(task.set_state(TaskState::Running).is_ok());
596 assert!(task.set_state(TaskState::Completed).is_ok());
597 assert!(task.set_state(TaskState::Running).is_err());
598 }
599
600 #[test]
601 fn test_task_retry_logic() {
602 let mut task = Task::new(
603 "test",
604 TaskType::Wait {
605 duration: Duration::from_secs(1),
606 },
607 )
608 .with_retry(RetryPolicy {
609 max_attempts: 2,
610 ..Default::default()
611 });
612
613 assert!(task.should_retry());
614 task.increment_retry();
615 assert!(task.should_retry());
616 task.increment_retry();
617 assert!(!task.should_retry());
618 }
619
620 #[test]
621 fn test_task_priority_ordering() {
622 assert!(TaskPriority::Critical > TaskPriority::High);
623 assert!(TaskPriority::High > TaskPriority::Normal);
624 assert!(TaskPriority::Normal > TaskPriority::Low);
625 }
626
627 #[test]
628 fn test_retry_policy_exponential_backoff() {
629 let policy = RetryPolicy {
630 max_attempts: 5,
631 initial_delay: Duration::from_secs(1),
632 max_delay: Duration::from_secs(60),
633 backoff_multiplier: 2.0,
634 exponential_backoff: true,
635 };
636
637 let delay0 = policy.delay_for_attempt(0);
638 let delay1 = policy.delay_for_attempt(1);
639 let delay2 = policy.delay_for_attempt(2);
640
641 assert_eq!(delay0.as_secs(), 1);
642 assert_eq!(delay1.as_secs(), 2);
643 assert_eq!(delay2.as_secs(), 4);
644 }
645}