Skip to main content

u_schedule/models/
task.rs

1//! Task (job) model.
2//!
3//! A task represents a unit of work to be scheduled, consisting of
4//! one or more activities (operations) with precedence constraints.
5//!
6//! # Reference
7//! Pinedo (2016), "Scheduling: Theory, Algorithms, and Systems", Ch. 1
8
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use super::Activity;
13
14/// A task (job) to be scheduled.
15///
16/// Contains one or more activities and scheduling metadata (priority, deadlines).
17/// Activities within a task may have precedence constraints forming a DAG.
18///
19/// # Time Representation
20/// All times are in milliseconds relative to a scheduling epoch (t=0).
21/// The consumer defines what t=0 means (e.g., shift start, midnight UTC).
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Task {
24    /// Unique task identifier.
25    pub id: String,
26    /// Human-readable name.
27    pub name: String,
28    /// Task category (for transition matrix lookups and grouping).
29    pub category: String,
30    /// Scheduling priority (higher = more important).
31    pub priority: i32,
32    /// Latest completion time (ms). `None` = no deadline.
33    pub deadline: Option<i64>,
34    /// Earliest start time (ms). `None` = available immediately.
35    pub release_time: Option<i64>,
36    /// Activities (operations) that compose this task.
37    pub activities: Vec<Activity>,
38    /// Domain-specific key-value metadata.
39    pub attributes: HashMap<String, String>,
40}
41
42impl Task {
43    /// Creates a new task with the given ID.
44    pub fn new(id: impl Into<String>) -> Self {
45        Self {
46            id: id.into(),
47            name: String::new(),
48            category: String::new(),
49            priority: 0,
50            deadline: None,
51            release_time: None,
52            activities: Vec::new(),
53            attributes: HashMap::new(),
54        }
55    }
56
57    /// Sets the task name.
58    pub fn with_name(mut self, name: impl Into<String>) -> Self {
59        self.name = name.into();
60        self
61    }
62
63    /// Sets the task category.
64    pub fn with_category(mut self, category: impl Into<String>) -> Self {
65        self.category = category.into();
66        self
67    }
68
69    /// Sets the scheduling priority.
70    pub fn with_priority(mut self, priority: i32) -> Self {
71        self.priority = priority;
72        self
73    }
74
75    /// Sets the deadline (latest completion time in ms).
76    pub fn with_deadline(mut self, deadline_ms: i64) -> Self {
77        self.deadline = Some(deadline_ms);
78        self
79    }
80
81    /// Sets the release time (earliest start time in ms).
82    pub fn with_release_time(mut self, release_ms: i64) -> Self {
83        self.release_time = Some(release_ms);
84        self
85    }
86
87    /// Adds an activity to this task.
88    pub fn with_activity(mut self, activity: Activity) -> Self {
89        self.activities.push(activity);
90        self
91    }
92
93    /// Adds a domain-specific attribute.
94    pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
95        self.attributes.insert(key.into(), value.into());
96        self
97    }
98
99    /// Total processing duration across all activities (ms).
100    pub fn total_duration_ms(&self) -> i64 {
101        self.activities.iter().map(|a| a.duration.total_ms()).sum()
102    }
103
104    /// Whether this task has any activities.
105    pub fn has_activities(&self) -> bool {
106        !self.activities.is_empty()
107    }
108
109    /// Number of activities.
110    pub fn activity_count(&self) -> usize {
111        self.activities.len()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::models::ActivityDuration;
119
120    #[test]
121    fn test_task_builder() {
122        let task = Task::new("J1")
123            .with_name("Job 1")
124            .with_category("TypeA")
125            .with_priority(10)
126            .with_deadline(100_000)
127            .with_release_time(0)
128            .with_attribute("customer", "ACME");
129
130        assert_eq!(task.id, "J1");
131        assert_eq!(task.name, "Job 1");
132        assert_eq!(task.category, "TypeA");
133        assert_eq!(task.priority, 10);
134        assert_eq!(task.deadline, Some(100_000));
135        assert_eq!(task.release_time, Some(0));
136        assert_eq!(task.attributes.get("customer"), Some(&"ACME".to_string()));
137    }
138
139    #[test]
140    fn test_task_total_duration() {
141        let task = Task::new("J1")
142            .with_activity(
143                Activity::new("O1", "J1", 0).with_duration(ActivityDuration::fixed(1000)),
144            )
145            .with_activity(
146                Activity::new("O2", "J1", 1).with_duration(ActivityDuration::fixed(2000)),
147            );
148
149        assert_eq!(task.total_duration_ms(), 3000);
150        assert_eq!(task.activity_count(), 2);
151        assert!(task.has_activities());
152    }
153
154    #[test]
155    fn test_task_empty() {
156        let task = Task::new("empty");
157        assert_eq!(task.total_duration_ms(), 0);
158        assert!(!task.has_activities());
159    }
160}