use crate::error::{Result, WorkflowError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TaskId(Uuid);
impl TaskId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[must_use]
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for TaskId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for TaskId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<Uuid> for TaskId {
fn from(uuid: Uuid) -> Self {
Self(uuid)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum TaskState {
#[default]
Pending,
Queued,
Running,
Completed,
Failed,
Cancelled,
Waiting,
Retrying,
Skipped,
}
impl TaskState {
#[must_use]
pub const fn is_terminal(&self) -> bool {
matches!(
self,
Self::Completed | Self::Failed | Self::Cancelled | Self::Skipped
)
}
#[must_use]
pub const fn is_active(&self) -> bool {
matches!(self, Self::Running | Self::Retrying)
}
#[must_use]
pub const fn can_start(&self) -> bool {
matches!(self, Self::Pending | Self::Queued)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
pub enum TaskPriority {
Low = 0,
#[default]
Normal = 1,
High = 2,
Critical = 3,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub initial_delay: Duration,
pub max_delay: Duration,
pub backoff_multiplier: f64,
pub exponential_backoff: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
exponential_backoff: true,
}
}
}
impl RetryPolicy {
#[must_use]
pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
if !self.exponential_backoff {
return self.initial_delay;
}
let delay = self.initial_delay.as_secs_f64()
* self
.backoff_multiplier
.powi(i32::try_from(attempt).unwrap_or(10));
let delay = delay.min(self.max_delay.as_secs_f64());
Duration::from_secs_f64(delay)
}
#[must_use]
pub const fn should_retry(&self, attempt: u32) -> bool {
attempt < self.max_attempts
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TaskType {
Transcode {
input: PathBuf,
output: PathBuf,
preset: String,
#[serde(default)]
params: HashMap<String, serde_json::Value>,
},
QualityControl {
input: PathBuf,
profile: String,
#[serde(default)]
rules: Vec<String>,
},
Transfer {
source: String,
destination: String,
protocol: TransferProtocol,
#[serde(default)]
options: HashMap<String, String>,
},
Notification {
channel: NotificationChannel,
message: String,
#[serde(default)]
metadata: HashMap<String, String>,
},
CustomScript {
script: PathBuf,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
env: HashMap<String, String>,
},
Analysis {
input: PathBuf,
analyses: Vec<AnalysisType>,
output: Option<PathBuf>,
},
Conditional {
condition: String,
true_task: Option<Box<Task>>,
false_task: Option<Box<Task>>,
},
Wait {
duration: Duration,
},
HttpRequest {
url: String,
method: HttpMethod,
#[serde(default)]
headers: HashMap<String, String>,
body: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TransferProtocol {
Local,
Ftp,
Sftp,
S3,
Http,
Rsync,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NotificationChannel {
Email {
to: Vec<String>,
subject: String,
},
Webhook {
url: String,
},
Slack {
channel: String,
webhook_url: String,
},
Discord {
webhook_url: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnalysisType {
AudioLevels,
VideoQuality,
SceneDetection,
BlackFrames,
Silence,
Color,
Motion,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: TaskId,
pub name: String,
pub task_type: TaskType,
#[serde(default)]
pub state: TaskState,
#[serde(default)]
pub priority: TaskPriority,
#[serde(default)]
pub retry: RetryPolicy,
#[serde(default = "default_timeout")]
pub timeout: Duration,
#[serde(default)]
pub dependencies: Vec<TaskId>,
#[serde(default)]
pub metadata: HashMap<String, String>,
#[serde(default)]
pub retry_count: u32,
#[serde(default)]
pub conditions: Vec<String>,
}
fn default_timeout() -> Duration {
Duration::from_secs(3600) }
impl Task {
#[must_use]
pub fn new(name: impl Into<String>, task_type: TaskType) -> Self {
Self {
id: TaskId::new(),
name: name.into(),
task_type,
state: TaskState::Pending,
priority: TaskPriority::default(),
retry: RetryPolicy::default(),
timeout: default_timeout(),
dependencies: Vec::new(),
metadata: HashMap::new(),
retry_count: 0,
conditions: Vec::new(),
}
}
pub fn add_dependency(&mut self, task_id: TaskId) {
if !self.dependencies.contains(&task_id) {
self.dependencies.push(task_id);
}
}
#[must_use]
pub fn with_priority(mut self, priority: TaskPriority) -> Self {
self.priority = priority;
self
}
#[must_use]
pub fn with_retry(mut self, retry: RetryPolicy) -> Self {
self.retry = retry;
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_condition(mut self, condition: impl Into<String>) -> Self {
self.conditions.push(condition.into());
self
}
#[must_use]
pub const fn can_execute(&self) -> bool {
self.state.can_start()
}
pub fn set_state(&mut self, state: TaskState) -> Result<()> {
match (&self.state, &state) {
(TaskState::Completed | TaskState::Failed | TaskState::Cancelled, _)
if !matches!(state, TaskState::Pending) =>
{
return Err(WorkflowError::InvalidStateTransition {
from: format!("{:?}", self.state),
to: format!("{state:?}"),
});
}
_ => {}
}
self.state = state;
Ok(())
}
pub fn increment_retry(&mut self) {
self.retry_count += 1;
}
#[must_use]
pub fn should_retry(&self) -> bool {
self.retry.should_retry(self.retry_count)
}
#[must_use]
pub fn retry_delay(&self) -> Duration {
self.retry.delay_for_attempt(self.retry_count)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskResult {
pub task_id: TaskId,
pub status: TaskState,
pub data: Option<serde_json::Value>,
pub error: Option<String>,
pub duration: Duration,
#[serde(default)]
pub outputs: Vec<PathBuf>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_task_id_creation() {
let id1 = TaskId::new();
let id2 = TaskId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_task_state_terminal() {
assert!(TaskState::Completed.is_terminal());
assert!(TaskState::Failed.is_terminal());
assert!(!TaskState::Running.is_terminal());
}
#[test]
fn test_task_state_active() {
assert!(TaskState::Running.is_active());
assert!(!TaskState::Pending.is_active());
}
#[test]
fn test_retry_policy_delay() {
let policy = RetryPolicy::default();
let delay1 = policy.delay_for_attempt(0);
let delay2 = policy.delay_for_attempt(1);
assert!(delay2 > delay1);
}
#[test]
fn test_retry_policy_max_attempts() {
let policy = RetryPolicy {
max_attempts: 3,
..Default::default()
};
assert!(policy.should_retry(0));
assert!(policy.should_retry(2));
assert!(!policy.should_retry(3));
}
#[test]
fn test_task_creation() {
let task = Task::new(
"test-task",
TaskType::Wait {
duration: Duration::from_secs(10),
},
);
assert_eq!(task.name, "test-task");
assert_eq!(task.state, TaskState::Pending);
}
#[test]
fn test_task_with_priority() {
let task = Task::new(
"test",
TaskType::Wait {
duration: Duration::from_secs(1),
},
)
.with_priority(TaskPriority::High);
assert_eq!(task.priority, TaskPriority::High);
}
#[test]
fn test_task_add_dependency() {
let mut task = Task::new(
"test",
TaskType::Wait {
duration: Duration::from_secs(1),
},
);
let dep_id = TaskId::new();
task.add_dependency(dep_id);
assert_eq!(task.dependencies.len(), 1);
assert_eq!(task.dependencies[0], dep_id);
}
#[test]
fn test_task_state_transition() {
let mut task = Task::new(
"test",
TaskType::Wait {
duration: Duration::from_secs(1),
},
);
assert!(task.set_state(TaskState::Running).is_ok());
assert!(task.set_state(TaskState::Completed).is_ok());
assert!(task.set_state(TaskState::Running).is_err());
}
#[test]
fn test_task_retry_logic() {
let mut task = Task::new(
"test",
TaskType::Wait {
duration: Duration::from_secs(1),
},
)
.with_retry(RetryPolicy {
max_attempts: 2,
..Default::default()
});
assert!(task.should_retry());
task.increment_retry();
assert!(task.should_retry());
task.increment_retry();
assert!(!task.should_retry());
}
#[test]
fn test_task_priority_ordering() {
assert!(TaskPriority::Critical > TaskPriority::High);
assert!(TaskPriority::High > TaskPriority::Normal);
assert!(TaskPriority::Normal > TaskPriority::Low);
}
#[test]
fn test_retry_policy_exponential_backoff() {
let policy = RetryPolicy {
max_attempts: 5,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
exponential_backoff: true,
};
let delay0 = policy.delay_for_attempt(0);
let delay1 = policy.delay_for_attempt(1);
let delay2 = policy.delay_for_attempt(2);
assert_eq!(delay0.as_secs(), 1);
assert_eq!(delay1.as_secs(), 2);
assert_eq!(delay2.as_secs(), 4);
}
}