Skip to main content

ojak/
queue.rs

1//! The queue work waits in until it is done: a backend the application picks.
2//!
3//! Ojak runs the loops — claiming, retrying, giving up — and keeps nothing
4//! between runs itself. What is queued lives in a [`Queue`], so that it
5//! survives a restart. As in Fedify, the queue is pluggable: *ojak-postgres*
6//! keeps it in a table of its own, [`MemoryQueue`] keeps it in memory for
7//! tests and for servers that can afford to lose it, and an application with
8//! tables for the purpose already implements the trait over them.
9//!
10//! Unlike Fedify's, a queue here is claimed from rather than listened to. A
11//! worker claims jobs for a lease and then reports each one done, to be
12//! retried after a delay, or failed. A lease is what lets any number of
13//! workers in any number of processes share one queue: the two colours of a
14//! blue/green deploy both run one, and what a colour was holding when it
15//! stopped is handed out again when the lease lapses, not lost.
16
17use serde_json::Value;
18use std::fmt;
19use std::future::Future;
20use std::sync::Mutex;
21use std::time::{Duration, Instant};
22
23/// A job, claimed for processing.
24#[derive(Clone, Debug, PartialEq)]
25pub struct Job {
26    /// The backend's identifier for it.
27    pub id: String,
28    /// Which queue it is in, such as `delivery`.
29    pub queue: String,
30    /// What the job is, as the code that queued it wrote it.
31    pub payload: Value,
32    /// How many times it has been tried before.
33    pub attempts: u32,
34}
35
36/// A failure in the queue backend itself.
37#[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
48/// A queue backend.
49///
50/// One backend holds any number of named queues, each claimed separately, so
51/// that deliveries and incoming activities can share a table without one
52/// starving the other.
53pub trait Queue: Send + Sync + 'static {
54    /// Queue `payloads` in `queue`, due now.
55    fn enqueue(
56        &self,
57        queue: &str,
58        payloads: Vec<Value>,
59    ) -> impl Future<Output = Result<(), QueueError>> + Send;
60
61    /// Queue `jobs` in `queue`, due now, each with its ordering key. A job is
62    /// not claimed while another with the same key, queued before it in any
63    /// queue, is still waiting, claimed or due to be retried; once that one
64    /// is complete or failed, the next is. Jobs with different keys, and
65    /// jobs with none, are claimed as they come due.
66    fn enqueue_ordered(
67        &self,
68        queue: &str,
69        jobs: Vec<(String, Value)>,
70    ) -> impl Future<Output = Result<(), QueueError>> + Send;
71
72    /// Claim up to `limit` due jobs from `queue` for `lease`. None of them is
73    /// claimed again until the lease lapses.
74    fn claim(
75        &self,
76        queue: &str,
77        limit: usize,
78        lease: Duration,
79    ) -> impl Future<Output = Result<Vec<Job>, QueueError>> + Send;
80
81    /// A job is done; it will not be seen again.
82    fn complete(&self, id: &str) -> impl Future<Output = Result<(), QueueError>> + Send;
83
84    /// A job is to be tried again after `delay`.
85    fn retry(
86        &self,
87        id: &str,
88        delay: Duration,
89        error: &str,
90    ) -> impl Future<Output = Result<(), QueueError>> + Send;
91
92    /// A job will not be tried again.
93    fn fail(&self, id: &str, error: &str) -> impl Future<Output = Result<(), QueueError>> + Send;
94
95    /// How long until the next job in `queue` is due, if one is waiting: how
96    /// long a worker with nothing to do may sleep.
97    fn next_due(
98        &self,
99        queue: &str,
100    ) -> impl Future<Output = Result<Option<Duration>, QueueError>> + Send;
101}
102
103/// When to try a failed job again: how many attempts a job gets, and how
104/// long to wait after each failure.
105#[derive(Clone, Copy, Debug)]
106pub struct RetryPolicy {
107    /// Attempts in all, the first included.
108    pub max_attempts: u32,
109    pub backoff: Backoff,
110}
111
112/// How long to wait before trying a failed job again.
113#[derive(Clone, Copy, Debug)]
114pub enum Backoff {
115    /// `initial` after the first failure, doubling after each one after
116    /// it, and never more than `max`.
117    Exponential { initial: Duration, max: Duration },
118    /// What the function says, given how many attempts have failed, one
119    /// for the first: for a schedule of the application's own, such as
120    /// another server's it is to behave as.
121    Custom(fn(u32) -> Duration),
122}
123
124impl Default for RetryPolicy {
125    /// Thirty seconds, doubling to an hour, twelve attempts: about eight
126    /// hours of a peer being down before a delivery is given up on.
127    fn default() -> Self {
128        Self::exponential(Duration::from_secs(30), Duration::from_secs(3600), 12)
129    }
130}
131
132impl RetryPolicy {
133    /// `max_attempts` attempts, waiting `initial` after the first failure and
134    /// doubling, up to `max`.
135    #[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    /// `max_attempts` attempts, waiting what `delay` says after each
144    /// failure, given how many attempts have failed.
145    #[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    /// The delay before the next attempt, after `attempts` attempts have
154    /// failed; `None` when there are to be no more.
155    #[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/// A [`Queue`] in memory.
171#[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/// A job's state in a [`MemoryQueue`], for tests to inspect.
199#[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    /// Every job the queue has held, in the order they were queued.
214    ///
215    /// # Panics
216    ///
217    /// When the queue's lock was poisoned by a panic elsewhere.
218    #[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        // The keys of the jobs not yet done, in the order they were queued:
299        // a job behind one of them waits.
300        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
354/// A [`Queue`] behind a pointer, for where the backend is chosen at run time:
355/// an application with a database per tenant hands Ojak a different queue
356/// for each request. Any [`Queue`] is one; [`shared`] makes a [`SharedQueue`].
357pub trait DynQueue: Send + Sync {
358    /// [`Queue::enqueue`].
359    fn enqueue<'a>(
360        &'a self,
361        queue: &'a str,
362        payloads: Vec<Value>,
363    ) -> BoxFuture<'a, Result<(), QueueError>>;
364    /// [`Queue::enqueue_ordered`].
365    fn enqueue_ordered<'a>(
366        &'a self,
367        queue: &'a str,
368        jobs: Vec<(String, Value)>,
369    ) -> BoxFuture<'a, Result<(), QueueError>>;
370    /// [`Queue::claim`].
371    fn claim<'a>(
372        &'a self,
373        queue: &'a str,
374        limit: usize,
375        lease: Duration,
376    ) -> BoxFuture<'a, Result<Vec<Job>, QueueError>>;
377    /// [`Queue::complete`].
378    fn complete<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<(), QueueError>>;
379    /// [`Queue::retry`].
380    fn retry<'a>(
381        &'a self,
382        id: &'a str,
383        delay: Duration,
384        error: &'a str,
385    ) -> BoxFuture<'a, Result<(), QueueError>>;
386    /// [`Queue::fail`].
387    fn fail<'a>(&'a self, id: &'a str, error: &'a str) -> BoxFuture<'a, Result<(), QueueError>>;
388    /// [`Queue::next_due`].
389    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
446/// A queue shared behind a pointer; see [`DynQueue`].
447pub type SharedQueue = std::sync::Arc<dyn DynQueue>;
448
449/// `queue` as a [`SharedQueue`].
450pub 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}