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.stored_payload(),
178 attempts: job.attempts,
179 error: error.to_string(),
180 failed_at: unix_now(),
181 });
182 Ok(())
183 })
184 }
185
186 fn failed_jobs(&self) -> BoxFuture<'_, Result<Vec<FailedJob>>> {
187 Box::pin(async move { Ok(self.state()?.failed.clone()) })
188 }
189
190 fn clear<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>> {
191 Box::pin(async move {
192 Ok(self
193 .state()?
194 .ready
195 .remove(queue)
196 .map(|entries| entries.len() as u64)
197 .unwrap_or(0))
198 })
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::job::Job;
206 use crate::queue::QueueExt;
207 use crate::tests_support::{CountingJob, counter};
208 use rustlavel_core::Json;
209
210 #[tokio::test]
211 async fn a_pushed_job_comes_back_out_intact() {
212 let queue = MemoryQueue::new();
213 let id = queue.dispatch(&CountingJob::new(counter(), 3)).await.unwrap();
214
215 assert_eq!(queue.size("default").await.unwrap(), 1);
216
217 let reserved = queue.pop("default").await.unwrap().expect("a job is waiting");
218 assert_eq!(reserved.id, id);
219 assert_eq!(reserved.job.name, CountingJob::NAME);
220 assert_eq!(reserved.attempts, 1);
221 assert_eq!(reserved.job.payload.get("step").and_then(Json::as_i64), Some(3));
222
223 queue.delete(&reserved).await.unwrap();
224 assert_eq!(queue.size("default").await.unwrap(), 0);
225 assert!(queue.pop("default").await.unwrap().is_none());
226 }
227
228 #[tokio::test]
229 async fn an_empty_queue_pops_nothing_rather_than_failing() {
230 let queue = MemoryQueue::new();
231 assert!(queue.pop("default").await.unwrap().is_none());
232 assert_eq!(queue.size("default").await.unwrap(), 0);
233 }
234
235 #[tokio::test]
236 async fn jobs_come_back_in_the_order_they_went_in() {
237 let queue = MemoryQueue::new();
238 for step in 1..=3 {
239 queue.dispatch(&CountingJob::new(counter(), step)).await.unwrap();
240 }
241
242 let mut seen = Vec::new();
243 while let Some(job) = queue.pop("default").await.unwrap() {
244 seen.push(job.job.payload.get("step").and_then(Json::as_i64).unwrap());
245 }
246
247 assert_eq!(seen, vec![1, 2, 3]);
248 }
249
250 #[tokio::test]
251 async fn queues_are_separate_from_each_other() {
252 let queue = MemoryQueue::new();
253 queue.push(QueuedJob::new("a", Json::Null)).await.unwrap();
254 queue.push(QueuedJob::new("b", Json::Null).on_queue("emails")).await.unwrap();
255
256 assert_eq!(queue.size("default").await.unwrap(), 1);
257 assert_eq!(queue.size("emails").await.unwrap(), 1);
258 assert_eq!(queue.pop("emails").await.unwrap().unwrap().job.name, "b");
259 assert!(queue.pop("emails").await.unwrap().is_none());
260 }
261
262 #[tokio::test]
263 async fn push_on_overrides_the_queue_the_job_asked_for() {
264 let queue = MemoryQueue::new();
265 queue.push_on("urgent", QueuedJob::new("a", Json::Null)).await.unwrap();
266
267 assert_eq!(queue.size("default").await.unwrap(), 0);
268 assert_eq!(queue.size("urgent").await.unwrap(), 1);
269 }
270
271 #[tokio::test]
272 async fn a_delayed_job_is_counted_but_not_yet_visible() {
273 let queue = MemoryQueue::new();
274 queue
275 .later(Duration::from_secs(3600), QueuedJob::new("later", Json::Null))
276 .await
277 .unwrap();
278
279 assert_eq!(queue.size("default").await.unwrap(), 1, "it is on the queue");
280 assert_eq!(queue.ready_size("default").unwrap(), 0, "but not ready");
281 assert!(queue.pop("default").await.unwrap().is_none(), "and not poppable");
282 }
283
284 #[tokio::test]
285 async fn a_delay_that_has_already_passed_is_visible_immediately() {
286 let queue = MemoryQueue::new();
287 queue.later(Duration::ZERO, QueuedJob::new("now", Json::Null)).await.unwrap();
288
289 assert!(queue.pop("default").await.unwrap().is_some());
290 }
291
292 #[tokio::test]
293 async fn a_delayed_job_does_not_block_a_ready_one_behind_it() {
294 let queue = MemoryQueue::new();
295 queue
296 .later(Duration::from_secs(3600), QueuedJob::new("later", Json::Null))
297 .await
298 .unwrap();
299 queue.push(QueuedJob::new("now", Json::Null)).await.unwrap();
300
301 assert_eq!(queue.pop("default").await.unwrap().unwrap().job.name, "now");
302 assert!(queue.pop("default").await.unwrap().is_none());
303 }
304
305 #[tokio::test]
306 async fn releasing_keeps_the_attempt_count() {
307 let queue = MemoryQueue::new();
308 queue.push(QueuedJob::new("retry-me", Json::Null)).await.unwrap();
309
310 let first = queue.pop("default").await.unwrap().unwrap();
311 assert_eq!(first.attempts, 1);
312 queue.release(&first, Duration::ZERO).await.unwrap();
313
314 let second = queue.pop("default").await.unwrap().unwrap();
315 assert_eq!(second.attempts, 2, "a retry continues counting");
316 assert_eq!(second.id, first.id, "and keeps its identity");
317 }
318
319 #[tokio::test]
320 async fn a_release_with_a_delay_is_not_immediately_visible() {
321 let queue = MemoryQueue::new();
322 queue.push(QueuedJob::new("retry-me", Json::Null)).await.unwrap();
323
324 let reserved = queue.pop("default").await.unwrap().unwrap();
325 queue.release(&reserved, Duration::from_secs(3600)).await.unwrap();
326
327 assert_eq!(queue.size("default").await.unwrap(), 1);
328 assert!(queue.pop("default").await.unwrap().is_none());
329 }
330
331 #[tokio::test]
332 async fn failing_moves_a_job_to_the_dead_letter_store() {
333 let queue = MemoryQueue::new();
334 queue.push(QueuedJob::new("doomed", Json::object([("x", Json::from(1))]))).await.unwrap();
335
336 let reserved = queue.pop("default").await.unwrap().unwrap();
337 queue.fail(&reserved, "it exploded").await.unwrap();
338
339 assert_eq!(queue.size("default").await.unwrap(), 0, "it is off the queue");
340
341 let failed = queue.failed_jobs().await.unwrap();
342 assert_eq!(failed.len(), 1);
343 assert_eq!(failed[0].name, "doomed");
344 assert_eq!(failed[0].error, "it exploded");
345 assert_eq!(failed[0].attempts, 1);
346 assert_eq!(failed[0].payload.get("x").and_then(Json::as_i64), Some(1));
347
348 assert_eq!(queue.flush_failed().unwrap(), 1);
349 assert!(queue.failed_jobs().await.unwrap().is_empty());
350 }
351
352 #[tokio::test]
353 async fn clearing_removes_only_the_named_queue() {
354 let queue = MemoryQueue::new();
355 queue.push(QueuedJob::new("a", Json::Null)).await.unwrap();
356 queue.push(QueuedJob::new("b", Json::Null)).await.unwrap();
357 queue.push(QueuedJob::new("c", Json::Null).on_queue("other")).await.unwrap();
358
359 assert_eq!(queue.clear("default").await.unwrap(), 2);
360 assert_eq!(queue.size("default").await.unwrap(), 0);
361 assert_eq!(queue.size("other").await.unwrap(), 1);
362 }
363
364 #[tokio::test]
365 async fn clones_share_one_store() {
366 let queue = MemoryQueue::new();
367 let clone = queue.clone();
368
369 queue.push(QueuedJob::new("shared", Json::Null)).await.unwrap();
370 assert_eq!(clone.size("default").await.unwrap(), 1);
371 }
372
373 #[tokio::test]
374 async fn the_driver_is_usable_behind_a_trait_object() {
375 let queue: Arc<dyn Queue> = Arc::new(MemoryQueue::new());
376
377 queue.dispatch(&CountingJob::new(counter(), 1)).await.unwrap();
378 assert_eq!(queue.size("default").await.unwrap(), 1);
379 assert_eq!(queue.driver(), "memory");
380 }
381}