1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! Job status tracking.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// Status of a background job.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum JobStatus {
/// Job is queued and waiting to be executed.
Pending,
/// Job is currently being executed.
Running {
/// When the job started executing.
started_at: DateTime<Utc>,
},
/// Job completed successfully.
Completed {
/// When the job completed.
completed_at: DateTime<Utc>,
},
/// Job failed and is being retried.
Retrying {
/// Number of attempts so far.
attempt: u32,
/// When the job last failed.
failed_at: DateTime<Utc>,
/// When the next retry will occur.
retry_at: DateTime<Utc>,
/// Error message from the last failure.
error: String,
},
/// Job failed permanently after exhausting retries.
Failed {
/// When the job finally failed.
failed_at: DateTime<Utc>,
/// Number of attempts made.
attempts: u32,
/// Final error message.
error: String,
},
/// Job was cancelled.
Cancelled {
/// When the job was cancelled.
cancelled_at: DateTime<Utc>,
},
}
impl JobStatus {
/// Check if the job is in a terminal state (completed, failed, or cancelled).
#[must_use]
pub const fn is_terminal(&self) -> bool {
matches!(
self,
Self::Completed { .. } | Self::Failed { .. } | Self::Cancelled { .. }
)
}
/// Check if the job is currently running.
#[must_use]
pub const fn is_running(&self) -> bool {
matches!(self, Self::Running { .. })
}
/// Check if the job is pending.
#[must_use]
pub const fn is_pending(&self) -> bool {
matches!(self, Self::Pending)
}
/// Check if the job is retrying.
#[must_use]
pub const fn is_retrying(&self) -> bool {
matches!(self, Self::Retrying { .. })
}
/// Get a human-readable status name.
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Running { .. } => "running",
Self::Completed { .. } => "completed",
Self::Retrying { .. } => "retrying",
Self::Failed { .. } => "failed",
Self::Cancelled { .. } => "cancelled",
}
}
}
impl Default for JobStatus {
fn default() -> Self {
Self::Pending
}
}
impl std::fmt::Display for JobStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_status_is_terminal() {
assert!(!JobStatus::Pending.is_terminal());
assert!(!JobStatus::Running {
started_at: Utc::now()
}
.is_terminal());
assert!(JobStatus::Completed {
completed_at: Utc::now()
}
.is_terminal());
assert!(JobStatus::Failed {
failed_at: Utc::now(),
attempts: 3,
error: "test error".to_string()
}
.is_terminal());
assert!(JobStatus::Cancelled {
cancelled_at: Utc::now()
}
.is_terminal());
}
#[test]
fn test_status_name() {
assert_eq!(JobStatus::Pending.name(), "pending");
assert_eq!(
JobStatus::Running {
started_at: Utc::now()
}
.name(),
"running"
);
assert_eq!(
JobStatus::Completed {
completed_at: Utc::now()
}
.name(),
"completed"
);
}
#[test]
fn test_status_display() {
let status = JobStatus::Pending;
assert_eq!(format!("{status}"), "pending");
}
}