Skip to main content

modo/job/
meta.rs

1use std::fmt;
2
3/// Job lifecycle status.
4///
5/// | Variant | Meaning |
6/// |---|---|
7/// | `Pending` | Waiting to be picked up by a worker |
8/// | `Running` | Currently being executed |
9/// | `Completed` | Finished successfully |
10/// | `Dead` | Exhausted all retry attempts |
11/// | `Cancelled` | Cancelled before execution via [`Enqueuer::cancel`](super::enqueuer::Enqueuer::cancel) |
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Status {
14    /// Waiting to be picked up by a worker.
15    Pending,
16    /// Currently being executed.
17    Running,
18    /// Finished successfully.
19    Completed,
20    /// Exhausted all retry attempts; will not be retried.
21    Dead,
22    /// Cancelled before execution.
23    Cancelled,
24}
25
26impl Status {
27    /// Returns the lowercase string representation of this status.
28    pub fn as_str(&self) -> &'static str {
29        match self {
30            Self::Pending => "pending",
31            Self::Running => "running",
32            Self::Completed => "completed",
33            Self::Dead => "dead",
34            Self::Cancelled => "cancelled",
35        }
36    }
37
38    /// Parses a status from its lowercase string representation.
39    ///
40    /// Returns `None` for unknown strings.
41    #[allow(clippy::should_implement_trait)]
42    pub fn from_str(s: &str) -> Option<Self> {
43        match s {
44            "pending" => Some(Self::Pending),
45            "running" => Some(Self::Running),
46            "completed" => Some(Self::Completed),
47            "dead" => Some(Self::Dead),
48            "cancelled" => Some(Self::Cancelled),
49            _ => None,
50        }
51    }
52
53    /// Returns `true` for statuses that are final: `Completed`, `Dead`, or
54    /// `Cancelled`.
55    pub fn is_terminal(&self) -> bool {
56        matches!(self, Self::Completed | Self::Dead | Self::Cancelled)
57    }
58}
59
60impl fmt::Display for Status {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.write_str(self.as_str())
63    }
64}
65
66/// Metadata about the currently executing job, available as a handler argument.
67///
68/// Extract `Meta` in a handler function to inspect the job's identity and
69/// retry state at runtime.
70#[derive(Debug, Clone)]
71pub struct Meta {
72    /// Unique job ID (ULID format).
73    pub id: String,
74    /// Registered handler name used to identify the job type.
75    pub name: String,
76    /// Name of the queue this job belongs to.
77    pub queue: String,
78    /// Current attempt number (1-based; incremented on each execution).
79    pub attempt: u32,
80    /// Maximum number of attempts before the job is marked `Dead`.
81    pub max_attempts: u32,
82    /// Absolute deadline for this execution; `None` if no timeout is set.
83    pub deadline: Option<tokio::time::Instant>,
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn status_roundtrip() {
92        let statuses = [
93            Status::Pending,
94            Status::Running,
95            Status::Completed,
96            Status::Dead,
97            Status::Cancelled,
98        ];
99        for s in &statuses {
100            let parsed = Status::from_str(s.as_str()).unwrap();
101            assert_eq!(&parsed, s);
102        }
103    }
104
105    #[test]
106    fn status_unknown_returns_none() {
107        assert!(Status::from_str("unknown").is_none());
108    }
109
110    #[test]
111    fn terminal_states() {
112        assert!(!Status::Pending.is_terminal());
113        assert!(!Status::Running.is_terminal());
114        assert!(Status::Completed.is_terminal());
115        assert!(Status::Dead.is_terminal());
116        assert!(Status::Cancelled.is_terminal());
117    }
118}