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
29#[derive(Debug, Clone)]
31pub struct JobInfo {
32 pub name: &'static str,
34 pub timeout: Duration,
36 pub priority: JobPriority,
38 pub retry: RetryConfig,
40 pub worker_capability: Option<&'static str>,
42 pub idempotent: bool,
44 pub idempotency_key: Option<&'static str>,
46 pub is_public: bool,
48 pub required_role: Option<&'static str>,
50}
51
52impl Default for JobInfo {
53 fn default() -> Self {
54 Self {
55 name: "",
56 timeout: Duration::from_secs(3600), priority: JobPriority::Normal,
58 retry: RetryConfig::default(),
59 worker_capability: None,
60 idempotent: false,
61 idempotency_key: None,
62 is_public: false,
63 required_role: None,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
70pub enum JobPriority {
71 Background = 0,
72 Low = 25,
73 #[default]
74 Normal = 50,
75 High = 75,
76 Critical = 100,
77}
78
79impl JobPriority {
80 pub fn as_i32(&self) -> i32 {
82 *self as i32
83 }
84
85 pub fn from_i32(value: i32) -> Self {
87 match value {
88 0..=12 => Self::Background,
89 13..=37 => Self::Low,
90 38..=62 => Self::Normal,
91 63..=87 => Self::High,
92 _ => Self::Critical,
93 }
94 }
95}
96
97impl FromStr for JobPriority {
98 type Err = std::convert::Infallible;
99
100 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
101 Ok(match s.to_lowercase().as_str() {
102 "background" => Self::Background,
103 "low" => Self::Low,
104 "normal" => Self::Normal,
105 "high" => Self::High,
106 "critical" => Self::Critical,
107 _ => Self::Normal,
108 })
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum JobStatus {
115 Pending,
117 Claimed,
119 Running,
121 Completed,
123 Retry,
125 Failed,
127 DeadLetter,
129}
130
131impl JobStatus {
132 pub fn as_str(&self) -> &'static str {
134 match self {
135 Self::Pending => "pending",
136 Self::Claimed => "claimed",
137 Self::Running => "running",
138 Self::Completed => "completed",
139 Self::Retry => "retry",
140 Self::Failed => "failed",
141 Self::DeadLetter => "dead_letter",
142 }
143 }
144}
145
146impl FromStr for JobStatus {
147 type Err = std::convert::Infallible;
148
149 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
150 Ok(match s {
151 "pending" => Self::Pending,
152 "claimed" => Self::Claimed,
153 "running" => Self::Running,
154 "completed" => Self::Completed,
155 "retry" => Self::Retry,
156 "failed" => Self::Failed,
157 "dead_letter" => Self::DeadLetter,
158 _ => Self::Pending,
159 })
160 }
161}
162
163#[derive(Debug, Clone)]
165pub struct RetryConfig {
166 pub max_attempts: u32,
168 pub backoff: BackoffStrategy,
170 pub max_backoff: Duration,
172 pub retry_on: Vec<String>,
174}
175
176impl Default for RetryConfig {
177 fn default() -> Self {
178 Self {
179 max_attempts: 3,
180 backoff: BackoffStrategy::Exponential,
181 max_backoff: Duration::from_secs(300), retry_on: Vec::new(), }
184 }
185}
186
187impl RetryConfig {
188 pub fn calculate_backoff(&self, attempt: u32) -> Duration {
190 let base = Duration::from_secs(1);
191 let backoff = match self.backoff {
192 BackoffStrategy::Fixed => base,
193 BackoffStrategy::Linear => base * attempt,
194 BackoffStrategy::Exponential => base * 2u32.pow(attempt.saturating_sub(1)),
195 };
196 backoff.min(self.max_backoff)
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
202pub enum BackoffStrategy {
203 Fixed,
205 Linear,
207 #[default]
209 Exponential,
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_priority_ordering() {
218 assert!(JobPriority::Critical > JobPriority::High);
219 assert!(JobPriority::High > JobPriority::Normal);
220 assert!(JobPriority::Normal > JobPriority::Low);
221 assert!(JobPriority::Low > JobPriority::Background);
222 }
223
224 #[test]
225 fn test_priority_conversion() {
226 assert_eq!(JobPriority::Critical.as_i32(), 100);
227 assert_eq!(JobPriority::Normal.as_i32(), 50);
228 assert_eq!(JobPriority::from_i32(100), JobPriority::Critical);
229 assert_eq!(JobPriority::from_i32(50), JobPriority::Normal);
230 }
231
232 #[test]
233 fn test_status_conversion() {
234 assert_eq!(JobStatus::Pending.as_str(), "pending");
235 assert_eq!("pending".parse::<JobStatus>(), Ok(JobStatus::Pending));
236 assert_eq!(JobStatus::DeadLetter.as_str(), "dead_letter");
237 assert_eq!(
238 "dead_letter".parse::<JobStatus>(),
239 Ok(JobStatus::DeadLetter)
240 );
241 }
242
243 #[test]
244 fn test_exponential_backoff() {
245 let config = RetryConfig::default();
246 assert_eq!(config.calculate_backoff(1), Duration::from_secs(1));
247 assert_eq!(config.calculate_backoff(2), Duration::from_secs(2));
248 assert_eq!(config.calculate_backoff(3), Duration::from_secs(4));
249 assert_eq!(config.calculate_backoff(4), Duration::from_secs(8));
250 }
251
252 #[test]
253 fn test_max_backoff_cap() {
254 let config = RetryConfig {
255 max_backoff: Duration::from_secs(10),
256 ..Default::default()
257 };
258 assert_eq!(config.calculate_backoff(10), Duration::from_secs(10));
259 }
260}