Skip to main content

rainier_queue/
queue.rs

1//! The [`Queue`] port and its in-process drivers.
2
3use std::collections::HashMap;
4use std::sync::Mutex;
5use std::time::Duration;
6
7use chrono::Utc;
8use rainier_support::{BoxFuture, Result};
9
10use crate::job::QueuedJob;
11
12/// A queue backend.
13///
14/// The contract is deliberately "reserve, then acknowledge" rather than "pop":
15/// a worker that crashes mid-job must not lose it. A reserved job stays in the
16/// store, invisible to other workers, until it is acknowledged, released or
17/// its reservation times out.
18pub trait Queue: Send + Sync + 'static {
19    /// A label for diagnostics — `"database"`, `"memory"`, `"sync"`.
20    fn name(&self) -> &str;
21
22    /// Enqueue a job.
23    fn push<'a>(&'a self, job: QueuedJob) -> BoxFuture<'a, Result<String>>;
24
25    /// Reserve the next available job on `queue`, or `None` if there is none.
26    fn reserve<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<Option<QueuedJob>>>;
27
28    /// Remove a finished job.
29    fn acknowledge<'a>(&'a self, job: &'a QueuedJob) -> BoxFuture<'a, Result<()>>;
30
31    /// Return a job to the queue for another attempt, available after `delay`.
32    fn release<'a>(&'a self, job: &'a QueuedJob, delay: Duration) -> BoxFuture<'a, Result<()>>;
33
34    /// Move a job to the failed store after its last attempt.
35    fn fail<'a>(&'a self, job: &'a QueuedJob, error: &'a str) -> BoxFuture<'a, Result<()>>;
36
37    /// How many jobs are waiting on `queue`.
38    fn size<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>>;
39
40    /// Discard everything on `queue`. Returns how many were removed.
41    fn clear<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>>;
42}
43
44/// A job that exhausted its attempts.
45#[derive(Debug, Clone, PartialEq)]
46pub struct FailedJob {
47    /// The job as it was when it failed.
48    pub job: QueuedJob,
49    /// The last error's message.
50    pub error: String,
51    /// When it failed.
52    pub failed_at: chrono::DateTime<Utc>,
53}
54
55/// An in-process queue.
56///
57/// Jobs live in this process's memory, so they are lost when it stops and are
58/// invisible to any other process. Right for tests and for a single-process
59/// development server; use the database driver for anything that must survive
60/// a restart.
61#[derive(Default)]
62pub struct MemoryQueue {
63    pending: Mutex<HashMap<String, Vec<QueuedJob>>>,
64    reserved: Mutex<Vec<QueuedJob>>,
65    failed: Mutex<Vec<FailedJob>>,
66}
67
68impl MemoryQueue {
69    /// An empty queue.
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// Every job that has exhausted its attempts.
75    pub fn failed_jobs(&self) -> Vec<FailedJob> {
76        self.failed.lock().expect("queue lock poisoned").clone()
77    }
78
79    /// How many jobs are currently reserved by a worker.
80    pub fn reserved_count(&self) -> usize {
81        self.reserved.lock().expect("queue lock poisoned").len()
82    }
83
84    /// Every pending job on `queue`, available or not.
85    pub fn pending(&self, queue: &str) -> Vec<QueuedJob> {
86        self.pending.lock().expect("queue lock poisoned").get(queue).cloned().unwrap_or_default()
87    }
88}
89
90impl Queue for MemoryQueue {
91    fn name(&self) -> &str {
92        "memory"
93    }
94
95    fn push<'a>(&'a self, job: QueuedJob) -> BoxFuture<'a, Result<String>> {
96        Box::pin(async move {
97            let id = job.id.clone();
98            self.pending
99                .lock()
100                .expect("queue lock poisoned")
101                .entry(job.queue.clone())
102                .or_default()
103                .push(job);
104            Ok(id)
105        })
106    }
107
108    fn reserve<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<Option<QueuedJob>>> {
109        Box::pin(async move {
110            let mut pending = self.pending.lock().expect("queue lock poisoned");
111            let Some(jobs) = pending.get_mut(queue) else {
112                return Ok(None);
113            };
114
115            // The first *available* job, not simply the first — a delayed job
116            // at the head must not block the ones behind it.
117            let Some(index) = jobs.iter().position(|job| job.is_available()) else {
118                return Ok(None);
119            };
120
121            let mut job = jobs.remove(index);
122            job.attempts += 1;
123            self.reserved.lock().expect("queue lock poisoned").push(job.clone());
124            Ok(Some(job))
125        })
126    }
127
128    fn acknowledge<'a>(&'a self, job: &'a QueuedJob) -> BoxFuture<'a, Result<()>> {
129        Box::pin(async move {
130            self.reserved
131                .lock()
132                .expect("queue lock poisoned")
133                .retain(|reserved| reserved.id != job.id);
134            Ok(())
135        })
136    }
137
138    fn release<'a>(&'a self, job: &'a QueuedJob, delay: Duration) -> BoxFuture<'a, Result<()>> {
139        Box::pin(async move {
140            self.reserved
141                .lock()
142                .expect("queue lock poisoned")
143                .retain(|reserved| reserved.id != job.id);
144
145            let released = job.clone().delayed_by(delay);
146            self.pending
147                .lock()
148                .expect("queue lock poisoned")
149                .entry(released.queue.clone())
150                .or_default()
151                .push(released);
152            Ok(())
153        })
154    }
155
156    fn fail<'a>(&'a self, job: &'a QueuedJob, error: &'a str) -> BoxFuture<'a, Result<()>> {
157        Box::pin(async move {
158            self.reserved
159                .lock()
160                .expect("queue lock poisoned")
161                .retain(|reserved| reserved.id != job.id);
162
163            self.failed.lock().expect("queue lock poisoned").push(FailedJob {
164                job: job.clone(),
165                error: error.to_string(),
166                failed_at: Utc::now(),
167            });
168            Ok(())
169        })
170    }
171
172    fn size<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>> {
173        Box::pin(async move {
174            Ok(self
175                .pending
176                .lock()
177                .expect("queue lock poisoned")
178                .get(queue)
179                .map(|jobs| jobs.len() as u64)
180                .unwrap_or(0))
181        })
182    }
183
184    fn clear<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>> {
185        Box::pin(async move {
186            let removed = self
187                .pending
188                .lock()
189                .expect("queue lock poisoned")
190                .remove(queue)
191                .map(|jobs| jobs.len() as u64)
192                .unwrap_or(0);
193            Ok(removed)
194        })
195    }
196}
197
198impl std::fmt::Debug for MemoryQueue {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        f.debug_struct("MemoryQueue")
201            .field("reserved", &self.reserved_count())
202            .field("failed", &self.failed.lock().map(|f| f.len()).unwrap_or(0))
203            .finish()
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use crate::job::{Job, JobContext};
211    use serde::{Deserialize, Serialize};
212
213    #[derive(Serialize, Deserialize)]
214    struct Ping {
215        n: u32,
216    }
217
218    #[async_trait::async_trait]
219    impl Job for Ping {
220        const NAME: &'static str = "test.ping";
221        async fn handle(&self, _: &JobContext) -> Result<()> {
222            Ok(())
223        }
224    }
225
226    fn ping(n: u32) -> QueuedJob {
227        QueuedJob::from_job(&Ping { n }).unwrap()
228    }
229
230    #[tokio::test]
231    async fn a_pushed_job_can_be_reserved() {
232        let queue = MemoryQueue::new();
233        queue.push(ping(1)).await.unwrap();
234
235        assert_eq!(queue.size("default").await.unwrap(), 1);
236        let reserved = queue.reserve("default").await.unwrap().expect("a job should be waiting");
237        assert_eq!(reserved.payload["n"], 1);
238    }
239
240    #[tokio::test]
241    async fn reserving_increments_the_attempt_count() {
242        let queue = MemoryQueue::new();
243        queue.push(ping(1)).await.unwrap();
244
245        let reserved = queue.reserve("default").await.unwrap().unwrap();
246        assert_eq!(reserved.attempts, 1);
247    }
248
249    #[tokio::test]
250    async fn a_reserved_job_is_invisible_to_other_workers() {
251        let queue = MemoryQueue::new();
252        queue.push(ping(1)).await.unwrap();
253
254        queue.reserve("default").await.unwrap().unwrap();
255        assert!(
256            queue.reserve("default").await.unwrap().is_none(),
257            "two workers must not get the same job"
258        );
259        assert_eq!(queue.reserved_count(), 1);
260    }
261
262    #[tokio::test]
263    async fn acknowledging_removes_it_for_good() {
264        let queue = MemoryQueue::new();
265        queue.push(ping(1)).await.unwrap();
266
267        let job = queue.reserve("default").await.unwrap().unwrap();
268        queue.acknowledge(&job).await.unwrap();
269
270        assert_eq!(queue.reserved_count(), 0);
271        assert_eq!(queue.size("default").await.unwrap(), 0);
272    }
273
274    #[tokio::test]
275    async fn releasing_puts_it_back_with_its_attempt_count_kept() {
276        let queue = MemoryQueue::new();
277        queue.push(ping(1)).await.unwrap();
278
279        let job = queue.reserve("default").await.unwrap().unwrap();
280        queue.release(&job, Duration::ZERO).await.unwrap();
281
282        assert_eq!(queue.reserved_count(), 0);
283        let again = queue.reserve("default").await.unwrap().unwrap();
284        assert_eq!(again.attempts, 2, "the retry counts as a further attempt");
285    }
286
287    #[tokio::test]
288    async fn releasing_with_a_delay_holds_it_back() {
289        let queue = MemoryQueue::new();
290        queue.push(ping(1)).await.unwrap();
291
292        let job = queue.reserve("default").await.unwrap().unwrap();
293        queue.release(&job, Duration::from_secs(60)).await.unwrap();
294
295        assert_eq!(queue.size("default").await.unwrap(), 1, "it is back in the queue");
296        assert!(queue.reserve("default").await.unwrap().is_none(), "but not yet available");
297    }
298
299    #[tokio::test]
300    async fn a_delayed_job_does_not_block_the_ones_behind_it() {
301        let queue = MemoryQueue::new();
302        queue.push(ping(1).delayed_by(Duration::from_secs(60))).await.unwrap();
303        queue.push(ping(2)).await.unwrap();
304
305        let reserved = queue.reserve("default").await.unwrap().expect("the ready job");
306        assert_eq!(reserved.payload["n"], 2);
307    }
308
309    #[tokio::test]
310    async fn failing_moves_it_to_the_failed_store() {
311        let queue = MemoryQueue::new();
312        queue.push(ping(1)).await.unwrap();
313
314        let job = queue.reserve("default").await.unwrap().unwrap();
315        queue.fail(&job, "connection refused").await.unwrap();
316
317        assert_eq!(queue.reserved_count(), 0);
318        let failed = queue.failed_jobs();
319        assert_eq!(failed.len(), 1);
320        assert_eq!(failed[0].error, "connection refused");
321        assert_eq!(failed[0].job.id, job.id);
322    }
323
324    #[tokio::test]
325    async fn queues_are_independent() {
326        let queue = MemoryQueue::new();
327        queue.push(ping(1).on_queue("emails")).await.unwrap();
328        queue.push(ping(2)).await.unwrap();
329
330        assert_eq!(queue.size("emails").await.unwrap(), 1);
331        assert_eq!(queue.size("default").await.unwrap(), 1);
332        assert_eq!(queue.size("nothing-here").await.unwrap(), 0);
333
334        let reserved = queue.reserve("emails").await.unwrap().unwrap();
335        assert_eq!(reserved.payload["n"], 1);
336    }
337
338    #[tokio::test]
339    async fn jobs_come_back_in_the_order_they_went_in() {
340        let queue = MemoryQueue::new();
341        for n in 1..=3 {
342            queue.push(ping(n)).await.unwrap();
343        }
344
345        for expected in 1..=3 {
346            let job = queue.reserve("default").await.unwrap().unwrap();
347            assert_eq!(job.payload["n"], expected);
348        }
349    }
350
351    #[tokio::test]
352    async fn clearing_discards_everything_pending() {
353        let queue = MemoryQueue::new();
354        queue.push(ping(1)).await.unwrap();
355        queue.push(ping(2)).await.unwrap();
356
357        assert_eq!(queue.clear("default").await.unwrap(), 2);
358        assert_eq!(queue.size("default").await.unwrap(), 0);
359    }
360
361    #[tokio::test]
362    async fn an_empty_queue_reserves_nothing() {
363        let queue = MemoryQueue::new();
364        assert!(queue.reserve("default").await.unwrap().is_none());
365        assert_eq!(queue.size("default").await.unwrap(), 0);
366    }
367}