1use std::future::Future;
2use std::pin::Pin;
3use std::str::FromStr;
4use std::time::Duration;
5
6use serde::{Serialize, de::DeserializeOwned};
7
8use crate::error::Result;
9
10use super::context::JobContext;
11
12pub trait ForgeJob: Send + Sync + 'static {
14 type Args: DeserializeOwned + Serialize + Send + Sync;
16 type Output: Serialize + Send;
18
19 fn info() -> JobInfo;
21
22 fn execute(
24 ctx: &JobContext,
25 args: Self::Args,
26 ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
27
28 fn compensate<'a>(
30 _ctx: &'a JobContext,
31 _args: Self::Args,
32 _reason: &'a str,
33 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
34 Box::pin(async { Ok(()) })
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct JobInfo {
41 pub name: &'static str,
43 pub timeout: Duration,
45 pub priority: JobPriority,
47 pub retry: RetryConfig,
49 pub worker_capability: Option<&'static str>,
51 pub idempotent: bool,
53 pub idempotency_key: Option<&'static str>,
55 pub is_public: bool,
57 pub required_role: Option<&'static str>,
59 pub ttl: Option<Duration>,
62}
63
64impl Default for JobInfo {
65 fn default() -> Self {
66 Self {
67 name: "",
68 timeout: Duration::from_secs(3600), priority: JobPriority::Normal,
70 retry: RetryConfig::default(),
71 worker_capability: None,
72 idempotent: false,
73 idempotency_key: None,
74 is_public: false,
75 required_role: None,
76 ttl: None,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
83pub enum JobPriority {
84 Background = 0,
85 Low = 25,
86 #[default]
87 Normal = 50,
88 High = 75,
89 Critical = 100,
90}
91
92impl JobPriority {
93 pub fn as_i32(&self) -> i32 {
95 *self as i32
96 }
97
98 pub fn from_i32(value: i32) -> Self {
100 match value {
101 0..=12 => Self::Background,
102 13..=37 => Self::Low,
103 38..=62 => Self::Normal,
104 63..=87 => Self::High,
105 _ => Self::Critical,
106 }
107 }
108}
109
110impl FromStr for JobPriority {
111 type Err = std::convert::Infallible;
112
113 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
114 Ok(match s.to_lowercase().as_str() {
115 "background" => Self::Background,
116 "low" => Self::Low,
117 "normal" => Self::Normal,
118 "high" => Self::High,
119 "critical" => Self::Critical,
120 _ => Self::Normal,
121 })
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum JobStatus {
128 Pending,
130 Claimed,
132 Running,
134 Completed,
136 Retry,
138 Failed,
140 DeadLetter,
142 CancelRequested,
144 Cancelled,
146}
147
148impl JobStatus {
149 pub fn as_str(&self) -> &'static str {
151 match self {
152 Self::Pending => "pending",
153 Self::Claimed => "claimed",
154 Self::Running => "running",
155 Self::Completed => "completed",
156 Self::Retry => "retry",
157 Self::Failed => "failed",
158 Self::DeadLetter => "dead_letter",
159 Self::CancelRequested => "cancel_requested",
160 Self::Cancelled => "cancelled",
161 }
162 }
163}
164
165impl FromStr for JobStatus {
166 type Err = std::convert::Infallible;
167
168 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
169 Ok(match s {
170 "pending" => Self::Pending,
171 "claimed" => Self::Claimed,
172 "running" => Self::Running,
173 "completed" => Self::Completed,
174 "retry" => Self::Retry,
175 "failed" => Self::Failed,
176 "dead_letter" => Self::DeadLetter,
177 "cancel_requested" => Self::CancelRequested,
178 "cancelled" => Self::Cancelled,
179 _ => Self::Pending,
180 })
181 }
182}
183
184#[derive(Debug, Clone)]
186pub struct RetryConfig {
187 pub max_attempts: u32,
189 pub backoff: BackoffStrategy,
191 pub max_backoff: Duration,
193 pub retry_on: Vec<String>,
195}
196
197impl Default for RetryConfig {
198 fn default() -> Self {
199 Self {
200 max_attempts: 3,
201 backoff: BackoffStrategy::Exponential,
202 max_backoff: Duration::from_secs(300), retry_on: Vec::new(), }
205 }
206}
207
208impl RetryConfig {
209 pub fn calculate_backoff(&self, attempt: u32) -> Duration {
211 let base = Duration::from_secs(1);
212 let backoff = match self.backoff {
213 BackoffStrategy::Fixed => base,
214 BackoffStrategy::Linear => base * attempt,
215 BackoffStrategy::Exponential => base * 2u32.pow(attempt.saturating_sub(1)),
216 };
217 backoff.min(self.max_backoff)
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
223pub enum BackoffStrategy {
224 Fixed,
226 Linear,
228 #[default]
230 Exponential,
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_priority_ordering() {
239 assert!(JobPriority::Critical > JobPriority::High);
240 assert!(JobPriority::High > JobPriority::Normal);
241 assert!(JobPriority::Normal > JobPriority::Low);
242 assert!(JobPriority::Low > JobPriority::Background);
243 }
244
245 #[test]
246 fn test_priority_conversion() {
247 assert_eq!(JobPriority::Critical.as_i32(), 100);
248 assert_eq!(JobPriority::Normal.as_i32(), 50);
249 assert_eq!(JobPriority::from_i32(100), JobPriority::Critical);
250 assert_eq!(JobPriority::from_i32(50), JobPriority::Normal);
251 }
252
253 #[test]
254 fn test_status_conversion() {
255 assert_eq!(JobStatus::Pending.as_str(), "pending");
256 assert_eq!("pending".parse::<JobStatus>(), Ok(JobStatus::Pending));
257 assert_eq!(JobStatus::DeadLetter.as_str(), "dead_letter");
258 assert_eq!(
259 "dead_letter".parse::<JobStatus>(),
260 Ok(JobStatus::DeadLetter)
261 );
262 assert_eq!(JobStatus::CancelRequested.as_str(), "cancel_requested");
263 assert_eq!(
264 "cancel_requested".parse::<JobStatus>(),
265 Ok(JobStatus::CancelRequested)
266 );
267 assert_eq!(JobStatus::Cancelled.as_str(), "cancelled");
268 assert_eq!("cancelled".parse::<JobStatus>(), Ok(JobStatus::Cancelled));
269 }
270
271 #[test]
272 fn test_exponential_backoff() {
273 let config = RetryConfig::default();
274 assert_eq!(config.calculate_backoff(1), Duration::from_secs(1));
275 assert_eq!(config.calculate_backoff(2), Duration::from_secs(2));
276 assert_eq!(config.calculate_backoff(3), Duration::from_secs(4));
277 assert_eq!(config.calculate_backoff(4), Duration::from_secs(8));
278 }
279
280 #[test]
281 fn test_max_backoff_cap() {
282 let config = RetryConfig {
283 max_backoff: Duration::from_secs(10),
284 ..Default::default()
285 };
286 assert_eq!(config.calculate_backoff(10), Duration::from_secs(10));
287 }
288}