1use 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#[derive(Debug, Clone)]
22struct Entry {
23 id: String,
24 job: QueuedJob,
25 attempts: u32,
26 available_at: i64,
28}
29
30#[derive(Default)]
31struct State {
32 ready: HashMap<String, Vec<Entry>>,
35 failed: Vec<FailedJob>,
36}
37
38struct Inner {
39 state: Mutex<State>,
40 next_id: AtomicU64,
41}
42
43#[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 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 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 let Some(index) = entries.iter().position(|entry| entry.available_at <= now) else {
126 return Ok(None);
127 };
128
129 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 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}