Skip to main content

rustlavel_queue/
memory.rs

1//! The in-process driver: the default, and the one tests use.
2//!
3//! Everything lives behind one `Mutex`. A queue is not a cache — it is a
4//! sequencing device, and every operation on it has to agree with every other
5//! about what the head of the queue is — so sharding would buy nothing but a
6//! way to hand the same job to two workers.
7//!
8//! No lock is ever held across an `await`, which is why a plain `std` mutex is
9//! the right one here rather than Tokio's.
10
11use crate::job::{BoxFuture, FailedJob, QueuedJob, ReservedJob};
12use crate::queue::{Queue, record_pushed};
13use crate::time::unix_now;
14use rustlavel_core::{Error, Result};
15use std::collections::HashMap;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::{Arc, Mutex};
18use std::time::Duration;
19
20/// A job waiting on a queue.
21#[derive(Debug, Clone)]
22struct Entry {
23    id: String,
24    job: QueuedJob,
25    attempts: u32,
26    /// Epoch seconds before which no worker may see this job.
27    available_at: i64,
28}
29
30#[derive(Default)]
31struct State {
32    /// Keyed by queue name; each list is kept in insertion order, so a queue
33    /// with no delays is strictly first-in-first-out.
34    ready: HashMap<String, Vec<Entry>>,
35    failed: Vec<FailedJob>,
36}
37
38struct Inner {
39    state: Mutex<State>,
40    next_id: AtomicU64,
41}
42
43/// A process-local queue. Cloning shares one store, so it can be registered as
44/// application state and handed to every handler and worker.
45#[derive(Clone)]
46pub struct MemoryQueue {
47    inner: Arc<Inner>,
48}
49
50impl Default for MemoryQueue {
51    fn default() -> Self {
52        MemoryQueue::new()
53    }
54}
55
56impl MemoryQueue {
57    pub fn new() -> Self {
58        MemoryQueue {
59            inner: Arc::new(Inner {
60                state: Mutex::new(State::default()),
61                next_id: AtomicU64::new(1),
62            }),
63        }
64    }
65
66    fn state(&self) -> Result<std::sync::MutexGuard<'_, State>> {
67        self.inner
68            .state
69            .lock()
70            .map_err(|_| Error::msg("the in-memory queue was poisoned by a panic while locked"))
71    }
72
73    /// How many jobs are ready to run right now, as opposed to [`Queue::size`],
74    /// which counts delayed ones too. Useful for asserting in a test that a
75    /// delay is actually holding a job back.
76    pub fn ready_size(&self, queue: &str) -> Result<u64> {
77        let now = unix_now();
78        let state = self.state()?;
79        Ok(state
80            .ready
81            .get(queue)
82            .map(|entries| entries.iter().filter(|e| e.available_at <= now).count() as u64)
83            .unwrap_or(0))
84    }
85
86    /// Empty the dead-letter store. `queue:flush` in Laravel.
87    pub fn flush_failed(&self) -> Result<u64> {
88        let mut state = self.state()?;
89        let count = state.failed.len() as u64;
90        state.failed.clear();
91        Ok(count)
92    }
93}
94
95impl Queue for MemoryQueue {
96    fn driver(&self) -> &'static str {
97        "memory"
98    }
99
100    fn push(&self, job: QueuedJob) -> BoxFuture<'_, Result<String>> {
101        Box::pin(async move {
102            let id = self.inner.next_id.fetch_add(1, Ordering::SeqCst).to_string();
103            let entry = Entry {
104                id: id.clone(),
105                available_at: unix_now() + job.delay.as_secs() as i64,
106                job: job.clone(),
107                attempts: 0,
108            };
109
110            self.state()?.ready.entry(job.queue.clone()).or_default().push(entry);
111
112            record_pushed(self.driver(), &job, &id);
113            Ok(id)
114        })
115    }
116
117    fn pop<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<Option<ReservedJob>>> {
118        Box::pin(async move {
119            let now = unix_now();
120            let mut state = self.state()?;
121            let Some(entries) = state.ready.get_mut(queue) else { return Ok(None) };
122
123            // The first *available* job, not the first job: a delayed one at the
124            // head must not block the ready one behind it.
125            let Some(index) = entries.iter().position(|entry| entry.available_at <= now) else {
126                return Ok(None);
127            };
128
129            // Removing rather than flagging is safe here because the store dies
130            // with the process: there is no crashed-worker case to recover, and
131            // a job in a worker's hand is a job no other worker can see.
132            let entry = entries.remove(index);
133
134            Ok(Some(ReservedJob {
135                id: entry.id,
136                job: entry.job,
137                attempts: entry.attempts + 1,
138            }))
139        })
140    }
141
142    fn size<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>> {
143        Box::pin(async move {
144            Ok(self.state()?.ready.get(queue).map(|entries| entries.len() as u64).unwrap_or(0))
145        })
146    }
147
148    fn delete<'a>(&'a self, _job: &'a ReservedJob) -> BoxFuture<'a, Result<()>> {
149        // Reserving already removed it. Kept as a no-op so the worker's flow is
150        // the same against every driver.
151        Box::pin(async { Ok(()) })
152    }
153
154    fn release<'a>(&'a self, job: &'a ReservedJob, delay: Duration) -> BoxFuture<'a, Result<()>> {
155        Box::pin(async move {
156            let entry = Entry {
157                id: job.id.clone(),
158                job: job.job.clone(),
159                attempts: job.attempts,
160                available_at: unix_now() + delay.as_secs() as i64,
161            };
162
163            self.state()?.ready.entry(job.job.queue.clone()).or_default().push(entry);
164            Ok(())
165        })
166    }
167
168    fn fail<'a>(&'a self, job: &'a ReservedJob, error: &'a str) -> BoxFuture<'a, Result<()>> {
169        Box::pin(async move {
170            self.state()?.failed.push(FailedJob {
171                id: job.id.clone(),
172                name: job.job.name.clone(),
173                queue: job.job.queue.clone(),
174                payload: job.job.payload.clone(),
175                attempts: job.attempts,
176                error: error.to_string(),
177                failed_at: unix_now(),
178            });
179            Ok(())
180        })
181    }
182
183    fn failed_jobs(&self) -> BoxFuture<'_, Result<Vec<FailedJob>>> {
184        Box::pin(async move { Ok(self.state()?.failed.clone()) })
185    }
186
187    fn clear<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>> {
188        Box::pin(async move {
189            Ok(self
190                .state()?
191                .ready
192                .remove(queue)
193                .map(|entries| entries.len() as u64)
194                .unwrap_or(0))
195        })
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::job::Job;
203    use crate::queue::QueueExt;
204    use crate::tests_support::{CountingJob, counter};
205    use rustlavel_core::Json;
206
207    #[tokio::test]
208    async fn a_pushed_job_comes_back_out_intact() {
209        let queue = MemoryQueue::new();
210        let id = queue.dispatch(&CountingJob::new(counter(), 3)).await.unwrap();
211
212        assert_eq!(queue.size("default").await.unwrap(), 1);
213
214        let reserved = queue.pop("default").await.unwrap().expect("a job is waiting");
215        assert_eq!(reserved.id, id);
216        assert_eq!(reserved.job.name, CountingJob::NAME);
217        assert_eq!(reserved.attempts, 1);
218        assert_eq!(reserved.job.payload.get("step").and_then(Json::as_i64), Some(3));
219
220        queue.delete(&reserved).await.unwrap();
221        assert_eq!(queue.size("default").await.unwrap(), 0);
222        assert!(queue.pop("default").await.unwrap().is_none());
223    }
224
225    #[tokio::test]
226    async fn an_empty_queue_pops_nothing_rather_than_failing() {
227        let queue = MemoryQueue::new();
228        assert!(queue.pop("default").await.unwrap().is_none());
229        assert_eq!(queue.size("default").await.unwrap(), 0);
230    }
231
232    #[tokio::test]
233    async fn jobs_come_back_in_the_order_they_went_in() {
234        let queue = MemoryQueue::new();
235        for step in 1..=3 {
236            queue.dispatch(&CountingJob::new(counter(), step)).await.unwrap();
237        }
238
239        let mut seen = Vec::new();
240        while let Some(job) = queue.pop("default").await.unwrap() {
241            seen.push(job.job.payload.get("step").and_then(Json::as_i64).unwrap());
242        }
243
244        assert_eq!(seen, vec![1, 2, 3]);
245    }
246
247    #[tokio::test]
248    async fn queues_are_separate_from_each_other() {
249        let queue = MemoryQueue::new();
250        queue.push(QueuedJob::new("a", Json::Null)).await.unwrap();
251        queue.push(QueuedJob::new("b", Json::Null).on_queue("emails")).await.unwrap();
252
253        assert_eq!(queue.size("default").await.unwrap(), 1);
254        assert_eq!(queue.size("emails").await.unwrap(), 1);
255        assert_eq!(queue.pop("emails").await.unwrap().unwrap().job.name, "b");
256        assert!(queue.pop("emails").await.unwrap().is_none());
257    }
258
259    #[tokio::test]
260    async fn push_on_overrides_the_queue_the_job_asked_for() {
261        let queue = MemoryQueue::new();
262        queue.push_on("urgent", QueuedJob::new("a", Json::Null)).await.unwrap();
263
264        assert_eq!(queue.size("default").await.unwrap(), 0);
265        assert_eq!(queue.size("urgent").await.unwrap(), 1);
266    }
267
268    #[tokio::test]
269    async fn a_delayed_job_is_counted_but_not_yet_visible() {
270        let queue = MemoryQueue::new();
271        queue
272            .later(Duration::from_secs(3600), QueuedJob::new("later", Json::Null))
273            .await
274            .unwrap();
275
276        assert_eq!(queue.size("default").await.unwrap(), 1, "it is on the queue");
277        assert_eq!(queue.ready_size("default").unwrap(), 0, "but not ready");
278        assert!(queue.pop("default").await.unwrap().is_none(), "and not poppable");
279    }
280
281    #[tokio::test]
282    async fn a_delay_that_has_already_passed_is_visible_immediately() {
283        let queue = MemoryQueue::new();
284        queue.later(Duration::ZERO, QueuedJob::new("now", Json::Null)).await.unwrap();
285
286        assert!(queue.pop("default").await.unwrap().is_some());
287    }
288
289    #[tokio::test]
290    async fn a_delayed_job_does_not_block_a_ready_one_behind_it() {
291        let queue = MemoryQueue::new();
292        queue
293            .later(Duration::from_secs(3600), QueuedJob::new("later", Json::Null))
294            .await
295            .unwrap();
296        queue.push(QueuedJob::new("now", Json::Null)).await.unwrap();
297
298        assert_eq!(queue.pop("default").await.unwrap().unwrap().job.name, "now");
299        assert!(queue.pop("default").await.unwrap().is_none());
300    }
301
302    #[tokio::test]
303    async fn releasing_keeps_the_attempt_count() {
304        let queue = MemoryQueue::new();
305        queue.push(QueuedJob::new("retry-me", Json::Null)).await.unwrap();
306
307        let first = queue.pop("default").await.unwrap().unwrap();
308        assert_eq!(first.attempts, 1);
309        queue.release(&first, Duration::ZERO).await.unwrap();
310
311        let second = queue.pop("default").await.unwrap().unwrap();
312        assert_eq!(second.attempts, 2, "a retry continues counting");
313        assert_eq!(second.id, first.id, "and keeps its identity");
314    }
315
316    #[tokio::test]
317    async fn a_release_with_a_delay_is_not_immediately_visible() {
318        let queue = MemoryQueue::new();
319        queue.push(QueuedJob::new("retry-me", Json::Null)).await.unwrap();
320
321        let reserved = queue.pop("default").await.unwrap().unwrap();
322        queue.release(&reserved, Duration::from_secs(3600)).await.unwrap();
323
324        assert_eq!(queue.size("default").await.unwrap(), 1);
325        assert!(queue.pop("default").await.unwrap().is_none());
326    }
327
328    #[tokio::test]
329    async fn failing_moves_a_job_to_the_dead_letter_store() {
330        let queue = MemoryQueue::new();
331        queue.push(QueuedJob::new("doomed", Json::object([("x", Json::from(1))]))).await.unwrap();
332
333        let reserved = queue.pop("default").await.unwrap().unwrap();
334        queue.fail(&reserved, "it exploded").await.unwrap();
335
336        assert_eq!(queue.size("default").await.unwrap(), 0, "it is off the queue");
337
338        let failed = queue.failed_jobs().await.unwrap();
339        assert_eq!(failed.len(), 1);
340        assert_eq!(failed[0].name, "doomed");
341        assert_eq!(failed[0].error, "it exploded");
342        assert_eq!(failed[0].attempts, 1);
343        assert_eq!(failed[0].payload.get("x").and_then(Json::as_i64), Some(1));
344
345        assert_eq!(queue.flush_failed().unwrap(), 1);
346        assert!(queue.failed_jobs().await.unwrap().is_empty());
347    }
348
349    #[tokio::test]
350    async fn clearing_removes_only_the_named_queue() {
351        let queue = MemoryQueue::new();
352        queue.push(QueuedJob::new("a", Json::Null)).await.unwrap();
353        queue.push(QueuedJob::new("b", Json::Null)).await.unwrap();
354        queue.push(QueuedJob::new("c", Json::Null).on_queue("other")).await.unwrap();
355
356        assert_eq!(queue.clear("default").await.unwrap(), 2);
357        assert_eq!(queue.size("default").await.unwrap(), 0);
358        assert_eq!(queue.size("other").await.unwrap(), 1);
359    }
360
361    #[tokio::test]
362    async fn clones_share_one_store() {
363        let queue = MemoryQueue::new();
364        let clone = queue.clone();
365
366        queue.push(QueuedJob::new("shared", Json::Null)).await.unwrap();
367        assert_eq!(clone.size("default").await.unwrap(), 1);
368    }
369
370    #[tokio::test]
371    async fn the_driver_is_usable_behind_a_trait_object() {
372        let queue: Arc<dyn Queue> = Arc::new(MemoryQueue::new());
373
374        queue.dispatch(&CountingJob::new(counter(), 1)).await.unwrap();
375        assert_eq!(queue.size("default").await.unwrap(), 1);
376        assert_eq!(queue.driver(), "memory");
377    }
378}