1use 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
12pub trait Queue: Send + Sync + 'static {
19 fn name(&self) -> &str;
21
22 fn push<'a>(&'a self, job: QueuedJob) -> BoxFuture<'a, Result<String>>;
24
25 fn reserve<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<Option<QueuedJob>>>;
27
28 fn acknowledge<'a>(&'a self, job: &'a QueuedJob) -> BoxFuture<'a, Result<()>>;
30
31 fn release<'a>(&'a self, job: &'a QueuedJob, delay: Duration) -> BoxFuture<'a, Result<()>>;
33
34 fn fail<'a>(&'a self, job: &'a QueuedJob, error: &'a str) -> BoxFuture<'a, Result<()>>;
36
37 fn size<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>>;
39
40 fn clear<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>>;
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub struct FailedJob {
47 pub job: QueuedJob,
49 pub error: String,
51 pub failed_at: chrono::DateTime<Utc>,
53}
54
55#[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 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn failed_jobs(&self) -> Vec<FailedJob> {
76 self.failed.lock().expect("queue lock poisoned").clone()
77 }
78
79 pub fn reserved_count(&self) -> usize {
81 self.reserved.lock().expect("queue lock poisoned").len()
82 }
83
84 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 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}