1use serde_json::Value;
18use std::fmt;
19use std::future::Future;
20use std::sync::Mutex;
21use std::time::{Duration, Instant};
22
23#[derive(Clone, Debug, PartialEq)]
25pub struct Job {
26 pub id: String,
28 pub queue: String,
30 pub payload: Value,
32 pub attempts: u32,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct QueueError(pub String);
39
40impl fmt::Display for QueueError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 f.write_str(&self.0)
43 }
44}
45
46impl std::error::Error for QueueError {}
47
48pub trait Queue: Send + Sync + 'static {
54 fn enqueue(
56 &self,
57 queue: &str,
58 payloads: Vec<Value>,
59 ) -> impl Future<Output = Result<(), QueueError>> + Send;
60
61 fn enqueue_ordered(
67 &self,
68 queue: &str,
69 jobs: Vec<(String, Value)>,
70 ) -> impl Future<Output = Result<(), QueueError>> + Send;
71
72 fn claim(
75 &self,
76 queue: &str,
77 limit: usize,
78 lease: Duration,
79 ) -> impl Future<Output = Result<Vec<Job>, QueueError>> + Send;
80
81 fn complete(&self, id: &str) -> impl Future<Output = Result<(), QueueError>> + Send;
83
84 fn retry(
86 &self,
87 id: &str,
88 delay: Duration,
89 error: &str,
90 ) -> impl Future<Output = Result<(), QueueError>> + Send;
91
92 fn fail(&self, id: &str, error: &str) -> impl Future<Output = Result<(), QueueError>> + Send;
94
95 fn next_due(
98 &self,
99 queue: &str,
100 ) -> impl Future<Output = Result<Option<Duration>, QueueError>> + Send;
101}
102
103#[derive(Clone, Copy, Debug)]
106pub struct RetryPolicy {
107 pub max_attempts: u32,
109 pub backoff: Backoff,
110}
111
112#[derive(Clone, Copy, Debug)]
114pub enum Backoff {
115 Exponential { initial: Duration, max: Duration },
118 Custom(fn(u32) -> Duration),
122}
123
124impl Default for RetryPolicy {
125 fn default() -> Self {
128 Self::exponential(Duration::from_secs(30), Duration::from_secs(3600), 12)
129 }
130}
131
132impl RetryPolicy {
133 #[must_use]
136 pub const fn exponential(initial: Duration, max: Duration, max_attempts: u32) -> Self {
137 Self {
138 max_attempts,
139 backoff: Backoff::Exponential { initial, max },
140 }
141 }
142
143 #[must_use]
146 pub const fn custom(delay: fn(u32) -> Duration, max_attempts: u32) -> Self {
147 Self {
148 max_attempts,
149 backoff: Backoff::Custom(delay),
150 }
151 }
152
153 #[must_use]
156 pub fn delay(&self, attempts: u32) -> Option<Duration> {
157 if attempts >= self.max_attempts {
158 return None;
159 }
160 Some(match self.backoff {
161 Backoff::Exponential { initial, max } => {
162 let doublings = attempts.saturating_sub(1).min(31);
163 initial.saturating_mul(1u32 << doublings).min(max)
164 }
165 Backoff::Custom(delay) => delay(attempts),
166 })
167 }
168}
169
170#[derive(Debug, Default)]
172pub struct MemoryQueue {
173 state: Mutex<MemoryState>,
174}
175
176#[derive(Debug, Default)]
177struct MemoryState {
178 next_id: u64,
179 entries: Vec<MemoryEntry>,
180}
181
182#[derive(Clone, Debug)]
183struct MemoryEntry {
184 job: Job,
185 ordering: Option<String>,
186 due: Instant,
187 status: MemoryStatus,
188 last_error: Option<String>,
189}
190
191#[derive(Clone, Copy, Debug, Eq, PartialEq)]
192enum MemoryStatus {
193 Waiting,
194 Complete,
195 Failed,
196}
197
198#[derive(Clone, Debug, PartialEq)]
200pub struct MemoryRecord {
201 pub job: Job,
202 pub complete: bool,
203 pub failed: bool,
204 pub last_error: Option<String>,
205}
206
207impl MemoryQueue {
208 #[must_use]
209 pub fn new() -> Self {
210 Self::default()
211 }
212
213 #[must_use]
219 pub fn records(&self) -> Vec<MemoryRecord> {
220 self.lock()
221 .entries
222 .iter()
223 .map(|entry| MemoryRecord {
224 job: entry.job.clone(),
225 complete: entry.status == MemoryStatus::Complete,
226 failed: entry.status == MemoryStatus::Failed,
227 last_error: entry.last_error.clone(),
228 })
229 .collect()
230 }
231
232 fn lock(&self) -> std::sync::MutexGuard<'_, MemoryState> {
233 self.state.lock().expect("memory queue lock")
234 }
235
236 fn update(&self, id: &str, change: impl FnOnce(&mut MemoryEntry)) -> Result<(), QueueError> {
237 let mut state = self.lock();
238 let entry = state
239 .entries
240 .iter_mut()
241 .find(|entry| entry.job.id == id)
242 .ok_or_else(|| QueueError(format!("no job {id}")))?;
243 change(entry);
244 Ok(())
245 }
246}
247
248impl MemoryQueue {
249 fn push(&self, queue: &str, jobs: impl IntoIterator<Item = (Option<String>, Value)>) {
250 let mut state = self.lock();
251 let now = Instant::now();
252 for (ordering, payload) in jobs {
253 state.next_id += 1;
254 let id = state.next_id.to_string();
255 state.entries.push(MemoryEntry {
256 job: Job {
257 id,
258 queue: queue.to_owned(),
259 payload,
260 attempts: 0,
261 },
262 ordering,
263 due: now,
264 status: MemoryStatus::Waiting,
265 last_error: None,
266 });
267 }
268 }
269}
270
271impl Queue for MemoryQueue {
272 async fn enqueue(&self, queue: &str, payloads: Vec<Value>) -> Result<(), QueueError> {
273 self.push(queue, payloads.into_iter().map(|payload| (None, payload)));
274 Ok(())
275 }
276
277 async fn enqueue_ordered(
278 &self,
279 queue: &str,
280 jobs: Vec<(String, Value)>,
281 ) -> Result<(), QueueError> {
282 self.push(
283 queue,
284 jobs.into_iter().map(|(key, payload)| (Some(key), payload)),
285 );
286 Ok(())
287 }
288
289 async fn claim(
290 &self,
291 queue: &str,
292 limit: usize,
293 lease: Duration,
294 ) -> Result<Vec<Job>, QueueError> {
295 let mut state = self.lock();
296 let now = Instant::now();
297 let mut claimed = Vec::new();
298 let mut ahead = std::collections::HashSet::new();
301 for entry in &mut state.entries {
302 if claimed.len() == limit {
303 break;
304 }
305 if entry.status != MemoryStatus::Waiting {
306 continue;
307 }
308 let waiting = match &entry.ordering {
309 Some(key) => !ahead.insert(key.clone()),
310 None => false,
311 };
312 if entry.job.queue == queue && !waiting && entry.due <= now {
313 entry.due = now + lease;
314 claimed.push(entry.job.clone());
315 }
316 }
317 Ok(claimed)
318 }
319
320 async fn complete(&self, id: &str) -> Result<(), QueueError> {
321 self.update(id, |entry| entry.status = MemoryStatus::Complete)
322 }
323
324 async fn retry(&self, id: &str, delay: Duration, error: &str) -> Result<(), QueueError> {
325 self.update(id, |entry| {
326 entry.job.attempts += 1;
327 entry.due = Instant::now() + delay;
328 entry.last_error = Some(error.to_owned());
329 })
330 }
331
332 async fn fail(&self, id: &str, error: &str) -> Result<(), QueueError> {
333 self.update(id, |entry| {
334 entry.job.attempts += 1;
335 entry.status = MemoryStatus::Failed;
336 entry.last_error = Some(error.to_owned());
337 })
338 }
339
340 async fn next_due(&self, queue: &str) -> Result<Option<Duration>, QueueError> {
341 let state = self.lock();
342 let now = Instant::now();
343 Ok(state
344 .entries
345 .iter()
346 .filter(|entry| entry.job.queue == queue && entry.status == MemoryStatus::Waiting)
347 .map(|entry| entry.due.saturating_duration_since(now))
348 .min())
349 }
350}
351
352type BoxFuture<'a, T> = std::pin::Pin<Box<dyn Future<Output = T> + Send + 'a>>;
353
354pub trait DynQueue: Send + Sync {
358 fn enqueue<'a>(
360 &'a self,
361 queue: &'a str,
362 payloads: Vec<Value>,
363 ) -> BoxFuture<'a, Result<(), QueueError>>;
364 fn enqueue_ordered<'a>(
366 &'a self,
367 queue: &'a str,
368 jobs: Vec<(String, Value)>,
369 ) -> BoxFuture<'a, Result<(), QueueError>>;
370 fn claim<'a>(
372 &'a self,
373 queue: &'a str,
374 limit: usize,
375 lease: Duration,
376 ) -> BoxFuture<'a, Result<Vec<Job>, QueueError>>;
377 fn complete<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<(), QueueError>>;
379 fn retry<'a>(
381 &'a self,
382 id: &'a str,
383 delay: Duration,
384 error: &'a str,
385 ) -> BoxFuture<'a, Result<(), QueueError>>;
386 fn fail<'a>(&'a self, id: &'a str, error: &'a str) -> BoxFuture<'a, Result<(), QueueError>>;
388 fn next_due<'a>(
390 &'a self,
391 queue: &'a str,
392 ) -> BoxFuture<'a, Result<Option<Duration>, QueueError>>;
393}
394
395impl<Q: Queue> DynQueue for Q {
396 fn enqueue<'a>(
397 &'a self,
398 queue: &'a str,
399 payloads: Vec<Value>,
400 ) -> BoxFuture<'a, Result<(), QueueError>> {
401 Box::pin(Queue::enqueue(self, queue, payloads))
402 }
403
404 fn enqueue_ordered<'a>(
405 &'a self,
406 queue: &'a str,
407 jobs: Vec<(String, Value)>,
408 ) -> BoxFuture<'a, Result<(), QueueError>> {
409 Box::pin(Queue::enqueue_ordered(self, queue, jobs))
410 }
411
412 fn claim<'a>(
413 &'a self,
414 queue: &'a str,
415 limit: usize,
416 lease: Duration,
417 ) -> BoxFuture<'a, Result<Vec<Job>, QueueError>> {
418 Box::pin(Queue::claim(self, queue, limit, lease))
419 }
420
421 fn complete<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<(), QueueError>> {
422 Box::pin(Queue::complete(self, id))
423 }
424
425 fn retry<'a>(
426 &'a self,
427 id: &'a str,
428 delay: Duration,
429 error: &'a str,
430 ) -> BoxFuture<'a, Result<(), QueueError>> {
431 Box::pin(Queue::retry(self, id, delay, error))
432 }
433
434 fn fail<'a>(&'a self, id: &'a str, error: &'a str) -> BoxFuture<'a, Result<(), QueueError>> {
435 Box::pin(Queue::fail(self, id, error))
436 }
437
438 fn next_due<'a>(
439 &'a self,
440 queue: &'a str,
441 ) -> BoxFuture<'a, Result<Option<Duration>, QueueError>> {
442 Box::pin(Queue::next_due(self, queue))
443 }
444}
445
446pub type SharedQueue = std::sync::Arc<dyn DynQueue>;
448
449pub fn shared(queue: impl Queue) -> SharedQueue {
451 std::sync::Arc::new(queue)
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn the_default_policy_doubles_to_an_hour_and_stops_at_twelve() {
460 let policy = RetryPolicy::default();
461 assert_eq!(policy.delay(1), Some(Duration::from_secs(30)));
462 assert_eq!(policy.delay(2), Some(Duration::from_secs(60)));
463 assert_eq!(policy.delay(7), Some(Duration::from_secs(1920)));
464 assert_eq!(policy.delay(8), Some(Duration::from_secs(3600)));
465 assert_eq!(policy.delay(11), Some(Duration::from_secs(3600)));
466 assert_eq!(policy.delay(12), None);
467 }
468
469 #[test]
470 fn a_custom_backoff_is_asked_how_many_have_failed() {
471 let policy = RetryPolicy::custom(|failed| Duration::from_secs(u64::from(failed) * 10), 3);
472 assert_eq!(policy.delay(1), Some(Duration::from_secs(10)));
473 assert_eq!(policy.delay(2), Some(Duration::from_secs(20)));
474 assert_eq!(policy.delay(3), None, "the third attempt was the last");
475 }
476}