sacs/
job.rs

1//! Contains primitive [`JobId`] which uniquely identifies executing `Task` instance.
2use crate::{task::TaskId, AsyncJobBoxed};
3use std::{
4    fmt::{Debug, Display},
5    time::Duration,
6};
7use uuid::Uuid;
8
9/// `JobId` uniquely identifies running instance of `Task`.
10///
11/// You don't need to construct this object manually:
12/// - `task_id` is provided from `Scheduler` during planned starting of the `Task` instance,
13/// - `job_id` is created automatically `Uuid`.
14///
15/// Executor creates `JobId` for each running job and provides it to job's closure as a parameter (`id` in the example below).
16///
17/// String representation of the `JobId` is `"{task_id}/{id}"`.
18///
19/// Common usage of `JobId` inside task closure is for logging.
20///
21/// # Examples
22///
23/// ```rust
24/// use sacs::task::{Task, TaskSchedule};
25/// use std::time::Duration;
26///
27/// let task = Task::new(TaskSchedule::Once, |id| {
28///     Box::pin(async move {
29///         println!("Starting job, TaskId={}, JobId={}.", id.task_id, id.id);
30///         // Actual async workload here
31///         tokio::time::sleep(Duration::from_secs(1)).await;
32///         // ...
33///         println!("Job {id} finished.");
34///         })
35///     });
36/// ```
37#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)]
38#[non_exhaustive]
39pub struct JobId {
40    /// ID of the `Task` which owns this Job, is provided from `Scheduler` during scheduled starting of the `Task` instance.
41    pub task_id: TaskId,
42    /// Unique ID of the running Job within particular `Task`.
43    pub id: Uuid,
44}
45
46impl JobId {
47    pub(crate) fn new(task_id: impl Into<TaskId>) -> Self {
48        Self {
49            id: Uuid::new_v4(),
50            task_id: task_id.into(),
51        }
52    }
53}
54
55impl From<TaskId> for JobId {
56    fn from(value: TaskId) -> Self {
57        Self {
58            id: Uuid::new_v4(),
59            task_id: value,
60        }
61    }
62}
63
64impl From<&TaskId> for JobId {
65    fn from(value: &TaskId) -> Self {
66        Self {
67            id: Uuid::new_v4(),
68            task_id: value.to_owned(),
69        }
70    }
71}
72
73impl From<JobId> for String {
74    fn from(value: JobId) -> Self {
75        format!("{}/{}", value.task_id, value.id)
76    }
77}
78
79impl Display for JobId {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(f, "{}/{}", self.task_id, self.id)
82    }
83}
84
85pub(crate) struct Job {
86    id: JobId,
87    job: AsyncJobBoxed,
88    timeout: Option<Duration>,
89}
90
91impl Job {
92    pub(crate) fn new(id: JobId, job: AsyncJobBoxed, timeout: Option<Duration>) -> Self {
93        Self { id, job, timeout }
94    }
95
96    pub(crate) fn id(&self) -> JobId {
97        self.id.clone()
98    }
99
100    pub(crate) fn job(&self) -> AsyncJobBoxed {
101        self.job.clone()
102    }
103
104    pub(crate) fn timeout(&self) -> Option<Duration> {
105        self.timeout
106    }
107}
108
109impl Debug for Job {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.debug_struct("Job")
112            .field("id", &self.id)
113            .field("timeout", &self.timeout)
114            .finish()
115    }
116}
117
118#[derive(Debug, Default, PartialEq, Clone)]
119pub(crate) enum JobState {
120    #[default]
121    Pending,
122    Starting,
123    Running,
124    Completed,
125    Canceled,
126    Timeout,
127    Error,
128}
129
130impl JobState {
131    pub fn finished(&self) -> bool {
132        *self == JobState::Completed
133            || *self == JobState::Canceled
134            || *self == JobState::Timeout
135            || *self == JobState::Error
136    }
137}
138
139#[cfg(test)]
140mod test {
141    use super::*;
142    use crate::task::{Task, TaskSchedule};
143
144    #[test]
145    fn job_state_finished() {
146        assert!(!JobState::Pending.finished());
147        assert!(!JobState::Starting.finished());
148        assert!(!JobState::Running.finished());
149        assert!(JobState::Completed.finished());
150        assert!(JobState::Canceled.finished());
151        assert!(JobState::Timeout.finished());
152        assert!(JobState::Error.finished());
153    }
154
155    #[test]
156    fn type_convertors() {
157        let task_id = TaskId::from("TASK_ID");
158        let job_id = JobId::from(task_id.clone());
159
160        assert_eq!(JobId::from(task_id.clone()).task_id, task_id);
161        assert_eq!(JobId::from(&task_id).task_id, task_id);
162
163        assert_eq!(
164            format!("{job_id}"),
165            format!("{}/{}", job_id.task_id, job_id.id)
166        );
167        assert_eq!(
168            String::from(job_id.clone()),
169            format!("{}/{}", job_id.task_id, job_id.id)
170        );
171    }
172
173    #[test]
174    fn debug_formatter() {
175        let task1 = Task::new(TaskSchedule::Once, |_id| Box::pin(async move {})).with_id("TEST");
176        let task2 = Task::new(TaskSchedule::Once, |_id| Box::pin(async move {}))
177            .with_id("TEST_WITH_TIMEOUT")
178            .with_timeout(Duration::from_secs(1));
179
180        let job1 = Job::new(JobId::new(task1.id()), task1.job, None);
181        let job2 = Job::new(
182            JobId::new(task2.id()),
183            task2.job,
184            Some(Duration::from_secs(1)),
185        );
186
187        assert_eq!(format!("{job1:?}"), format!("Job {{ id: JobId {{ task_id: TaskId {{ id: \"TEST\" }}, id: {} }}, timeout: None }}",job1.id().id));
188        assert_eq!(format!("{job2:?}"), format!("Job {{ id: JobId {{ task_id: TaskId {{ id: \"TEST_WITH_TIMEOUT\" }}, id: {} }}, timeout: Some(1s) }}", job2.id().id));
189    }
190}