1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
9#[serde(rename_all = "snake_case")]
10pub enum ScheduleKind {
11 #[default]
13 Cron,
14 RunOnce,
16 Manual,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct RetryPolicy {
26 pub max_attempts: u32,
28
29 pub base_delay_ms: u64,
31
32 pub backoff_multiplier: f64,
34
35 pub max_delay_ms: u64,
37}
38
39impl Default for RetryPolicy {
40 fn default() -> Self {
41 Self {
42 max_attempts: 0,
43 base_delay_ms: 0,
44 backoff_multiplier: 1.0,
45 max_delay_ms: 0,
46 }
47 }
48}
49
50impl RetryPolicy {
51 pub fn should_retry(&self, attempt: i32) -> bool {
53 attempt > 0 && (attempt as u32) <= self.max_attempts
54 }
55
56 pub fn delay_ms_after(&self, failed_attempt: i32) -> u64 {
58 let exp = failed_attempt.saturating_sub(1).max(0);
59 let raw = (self.base_delay_ms as f64) * self.backoff_multiplier.powi(exp);
60 let ms = if raw.is_finite() && raw > 0.0 {
61 raw.min(u64::MAX as f64) as u64
62 } else {
63 0
64 };
65 if self.max_delay_ms == 0 {
66 ms
67 } else {
68 ms.min(self.max_delay_ms)
69 }
70 }
71}
72
73#[derive(Debug, Clone, Default, Serialize, Deserialize)]
77pub struct MisfirePolicy {
78 pub run_immediately: bool,
80
81 pub max_misfire_window_secs: u64,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct Job {
90 pub job_id: String,
92
93 pub job_name: String,
95
96 pub script_name: String,
98
99 pub script_sig_hash: String,
101
102 pub enabled: bool,
104
105 pub schedule_kind: ScheduleKind,
107
108 pub cron_expr: Option<String>,
110
111 pub timezone: Option<String>,
113
114 pub run_once_at: Option<DateTime<Utc>>,
116
117 pub run_once_claimed_at: Option<DateTime<Utc>>,
119
120 pub run_once_claimed_by: Option<String>,
122
123 pub run_once_completed_at: Option<DateTime<Utc>>,
125
126 pub run_once_claim_expires_at: Option<DateTime<Utc>>,
128
129 pub partition_hash: Option<i64>,
131
132 pub claim_lease_id: Option<String>,
134
135 pub claim_lease_until: Option<DateTime<Utc>>,
137
138 pub pool: Option<String>,
141
142 pub region: Option<String>,
144
145 pub placement_json: Option<Value>,
147
148 pub actor_json: Value,
151
152 pub params_json: Value,
154
155 pub concurrency: i32,
157
158 pub timeout_ms: Option<i64>,
160
161 pub retry_policy_json: Value,
163
164 pub misfire_policy_json: Value,
166
167 pub parent_limits_json: Option<Value>,
169
170 pub next_run_at: Option<DateTime<Utc>>,
172
173 pub current_revision: i32,
175
176 pub updated_at: DateTime<Utc>,
178
179 pub created_at: DateTime<Utc>,
181}
182
183impl Job {
184 pub fn new(job_name: impl Into<String>, script_name: impl Into<String>) -> Self {
190 let now = Utc::now();
191 Self {
192 job_id: uuid::Uuid::new_v4().to_string(),
193 job_name: job_name.into(),
194 script_name: script_name.into(),
195 script_sig_hash: String::new(),
196 enabled: true,
197 schedule_kind: ScheduleKind::default(),
198 cron_expr: None,
199 timezone: None,
200 run_once_at: None,
201 run_once_claimed_at: None,
202 run_once_claimed_by: None,
203 run_once_completed_at: None,
204 run_once_claim_expires_at: None,
205 partition_hash: None,
206 claim_lease_id: None,
207 claim_lease_until: None,
208 pool: None,
209 region: None,
210 placement_json: None,
211 actor_json: Value::Null,
212 params_json: Value::Object(serde_json::Map::default()),
213 concurrency: 1,
214 timeout_ms: None,
215 retry_policy_json: serde_json::to_value(RetryPolicy::default()).unwrap_or_default(),
216 misfire_policy_json: serde_json::to_value(MisfirePolicy::default()).unwrap_or_default(),
217 parent_limits_json: None,
218 next_run_at: None,
219 current_revision: 1,
220 updated_at: now,
221 created_at: now,
222 }
223 }
224
225 pub fn retry_policy(&self) -> RetryPolicy {
227 serde_json::from_value(self.retry_policy_json.clone()).unwrap_or_default()
228 }
229
230 pub fn misfire_policy(&self) -> MisfirePolicy {
232 serde_json::from_value(self.misfire_policy_json.clone()).unwrap_or_default()
233 }
234
235 pub fn set_retry_policy(&mut self, policy: &RetryPolicy) {
237 self.retry_policy_json = serde_json::to_value(policy).unwrap_or_default();
238 }
239
240 pub fn set_misfire_policy(&mut self, policy: &MisfirePolicy) {
242 self.misfire_policy_json = serde_json::to_value(policy).unwrap_or_default();
243 }
244}
245
246#[cfg(test)]
247mod policy_tests {
248 use super::*;
249
250 #[test]
251 fn retry_default_does_not_retry() {
252 let p = RetryPolicy::default();
253 assert!(!p.should_retry(1));
254 assert_eq!(p.delay_ms_after(1), 0);
255 }
256
257 #[test]
258 fn retry_backoff_and_cap() {
259 let p = RetryPolicy {
260 max_attempts: 3,
261 base_delay_ms: 100,
262 backoff_multiplier: 2.0,
263 max_delay_ms: 250,
264 };
265 assert!(p.should_retry(1));
266 assert!(p.should_retry(3));
267 assert!(!p.should_retry(4));
268 assert_eq!(p.delay_ms_after(1), 100);
269 assert_eq!(p.delay_ms_after(2), 200);
270 assert_eq!(p.delay_ms_after(3), 250);
271 }
272
273 #[test]
274 fn job_policy_roundtrip() {
275 let mut job = Job::new("n", "s");
276 let retry = RetryPolicy {
277 max_attempts: 2,
278 base_delay_ms: 50,
279 backoff_multiplier: 1.5,
280 max_delay_ms: 500,
281 };
282 let misfire = MisfirePolicy {
283 run_immediately: true,
284 max_misfire_window_secs: 3600,
285 };
286 job.set_retry_policy(&retry);
287 job.set_misfire_policy(&misfire);
288 assert_eq!(job.retry_policy().max_attempts, 2);
289 assert!(job.misfire_policy().run_immediately);
290 assert_eq!(job.misfire_policy().max_misfire_window_secs, 3600);
291 }
292}