1#![forbid(unsafe_code)]
2
3use std::collections::{BTreeMap, HashMap, VecDeque};
20use std::fmt;
21use std::future::Future;
22use std::pin::Pin;
23use std::sync::{Arc, Mutex};
24use std::time::Duration;
25
26pub type QueueFuture<T> = Pin<Box<dyn Future<Output = Result<T, QueueError>> + Send + 'static>>;
27
28pub const DEAD_LETTER_SOURCE_HEADER: &str = "blazingly-dead-letter-source";
30pub const DEAD_LETTER_ATTEMPT_HEADER: &str = "blazingly-dead-letter-attempt";
32pub const DEAD_LETTER_REASON_HEADER: &str = "blazingly-dead-letter-reason";
34
35#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct Message {
38 pub body: Vec<u8>,
39 pub headers: BTreeMap<String, String>,
40}
41
42impl Message {
43 #[must_use]
44 pub fn new(body: impl Into<Vec<u8>>) -> Self {
45 Self {
46 body: body.into(),
47 headers: BTreeMap::new(),
48 }
49 }
50
51 #[must_use]
52 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
53 self.headers.insert(name.into(), value.into());
54 self
55 }
56}
57
58#[derive(Clone, Debug, Eq, PartialEq)]
60pub struct Delivery {
61 pub receipt: String,
62 pub attempt: u32,
63 pub message: Message,
64}
65
66pub trait Queue: Clone + Send + Sync + 'static {
69 fn publish(&self, topic: &str, message: Message) -> QueueFuture<()>;
71
72 fn receive(&self, topic: &str) -> QueueFuture<Option<Delivery>>;
74
75 fn ack(&self, receipt: &str) -> QueueFuture<()>;
77
78 fn nack(&self, receipt: &str, delay: Duration) -> QueueFuture<()>;
83}
84
85#[derive(Clone)]
87pub struct QueueClient<Adapter> {
88 adapter: Adapter,
89}
90
91impl<Adapter> QueueClient<Adapter> {
92 #[must_use]
93 pub const fn new(adapter: Adapter) -> Self {
94 Self { adapter }
95 }
96
97 #[must_use]
98 pub const fn adapter(&self) -> &Adapter {
99 &self.adapter
100 }
101}
102
103impl<Adapter: Queue> QueueClient<Adapter> {
104 pub fn publish(&self, topic: &str, message: Message) -> QueueFuture<()> {
105 self.adapter.publish(topic, message)
106 }
107
108 pub fn receive(&self, topic: &str) -> QueueFuture<Option<Delivery>> {
109 self.adapter.receive(topic)
110 }
111
112 pub fn ack(&self, receipt: &str) -> QueueFuture<()> {
113 self.adapter.ack(receipt)
114 }
115
116 pub fn nack(&self, receipt: &str, delay: Duration) -> QueueFuture<()> {
117 self.adapter.nack(receipt, delay)
118 }
119}
120
121#[derive(Clone, Default)]
126pub struct MemoryQueue {
127 state: Arc<Mutex<MemoryState>>,
128}
129
130#[derive(Default)]
131struct MemoryState {
132 next_receipt: u64,
133 now: Duration,
134 topics: HashMap<String, VecDeque<PendingDelivery>>,
135 in_flight: HashMap<String, (String, Delivery)>,
136}
137
138struct PendingDelivery {
139 visible_at: Duration,
140 delivery: Delivery,
141}
142
143impl MemoryQueue {
144 pub fn advance(&self, elapsed: Duration) {
146 let mut state = self.lock();
147 state.now = state.now.saturating_add(elapsed);
148 }
149
150 #[must_use]
152 pub fn depth(&self, topic: &str) -> usize {
153 self.lock().topics.get(topic).map_or(0, VecDeque::len)
154 }
155
156 #[must_use]
158 pub fn in_flight(&self) -> usize {
159 self.lock().in_flight.len()
160 }
161
162 fn lock(&self) -> std::sync::MutexGuard<'_, MemoryState> {
163 self.state
164 .lock()
165 .unwrap_or_else(std::sync::PoisonError::into_inner)
166 }
167}
168
169impl Queue for MemoryQueue {
170 fn publish(&self, topic: &str, message: Message) -> QueueFuture<()> {
171 let topic = topic.to_owned();
172 let state = Arc::clone(&self.state);
173 Box::pin(async move {
174 let mut state = state
175 .lock()
176 .unwrap_or_else(std::sync::PoisonError::into_inner);
177 state.next_receipt = state.next_receipt.wrapping_add(1);
178 let delivery = Delivery {
179 receipt: format!("memory-{}", state.next_receipt),
180 attempt: 1,
181 message,
182 };
183 let visible_at = state.now;
184 state
185 .topics
186 .entry(topic)
187 .or_default()
188 .push_back(PendingDelivery {
189 visible_at,
190 delivery,
191 });
192 Ok(())
193 })
194 }
195
196 fn receive(&self, topic: &str) -> QueueFuture<Option<Delivery>> {
197 let topic = topic.to_owned();
198 let state = Arc::clone(&self.state);
199 Box::pin(async move {
200 let mut state = state
201 .lock()
202 .unwrap_or_else(std::sync::PoisonError::into_inner);
203 let now = state.now;
204 let delivery = state.topics.get_mut(&topic).and_then(|pending| {
205 let index = pending
206 .iter()
207 .position(|candidate| candidate.visible_at <= now)?;
208 pending.remove(index).map(|entry| entry.delivery)
209 });
210 if let Some(delivery) = &delivery {
211 state
212 .in_flight
213 .insert(delivery.receipt.clone(), (topic, delivery.clone()));
214 }
215 Ok(delivery)
216 })
217 }
218
219 fn ack(&self, receipt: &str) -> QueueFuture<()> {
220 let receipt = receipt.to_owned();
221 let state = Arc::clone(&self.state);
222 Box::pin(async move {
223 state
224 .lock()
225 .unwrap_or_else(std::sync::PoisonError::into_inner)
226 .in_flight
227 .remove(&receipt)
228 .ok_or(QueueError::UnknownReceipt(receipt))?;
229 Ok(())
230 })
231 }
232
233 fn nack(&self, receipt: &str, delay: Duration) -> QueueFuture<()> {
234 let receipt = receipt.to_owned();
235 let state = Arc::clone(&self.state);
236 Box::pin(async move {
237 let mut state = state
238 .lock()
239 .unwrap_or_else(std::sync::PoisonError::into_inner);
240 let (topic, mut delivery) = state
241 .in_flight
242 .remove(&receipt)
243 .ok_or(QueueError::UnknownReceipt(receipt))?;
244 delivery.attempt = delivery.attempt.saturating_add(1);
245 let visible_at = state.now.saturating_add(delay);
246 state
247 .topics
248 .entry(topic)
249 .or_default()
250 .push_back(PendingDelivery {
251 visible_at,
252 delivery,
253 });
254 Ok(())
255 })
256 }
257}
258
259#[derive(Clone, Debug, Eq, PartialEq)]
261pub struct JobError {
262 message: String,
263 retryable: bool,
264}
265
266impl JobError {
267 #[must_use]
269 pub fn retryable(message: impl Into<String>) -> Self {
270 Self {
271 message: message.into(),
272 retryable: true,
273 }
274 }
275
276 #[must_use]
278 pub fn permanent(message: impl Into<String>) -> Self {
279 Self {
280 message: message.into(),
281 retryable: false,
282 }
283 }
284
285 #[must_use]
287 pub fn message(&self) -> &str {
288 &self.message
289 }
290
291 #[must_use]
293 pub const fn is_retryable(&self) -> bool {
294 self.retryable
295 }
296}
297
298impl fmt::Display for JobError {
299 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
300 formatter.write_str(&self.message)
301 }
302}
303
304impl std::error::Error for JobError {}
305
306#[derive(Clone, Copy, Debug, Eq, PartialEq)]
308pub struct RetryPolicy {
309 max_attempts: u32,
310 initial_backoff: Duration,
311 max_backoff: Duration,
312 multiplier: u32,
313}
314
315impl RetryPolicy {
316 #[must_use]
320 pub const fn new(max_attempts: u32) -> Self {
321 Self {
322 max_attempts: if max_attempts == 0 { 1 } else { max_attempts },
323 initial_backoff: Duration::from_secs(1),
324 max_backoff: Duration::from_secs(60),
325 multiplier: 2,
326 }
327 }
328
329 #[must_use]
331 pub const fn with_initial_backoff(mut self, initial_backoff: Duration) -> Self {
332 self.initial_backoff = initial_backoff;
333 self
334 }
335
336 #[must_use]
338 pub const fn with_max_backoff(mut self, max_backoff: Duration) -> Self {
339 self.max_backoff = max_backoff;
340 self
341 }
342
343 #[must_use]
345 pub const fn with_multiplier(mut self, multiplier: u32) -> Self {
346 self.multiplier = if multiplier == 0 { 1 } else { multiplier };
347 self
348 }
349
350 #[must_use]
352 pub const fn max_attempts(self) -> u32 {
353 self.max_attempts
354 }
355
356 #[must_use]
358 pub fn backoff(self, attempt: u32) -> Duration {
359 let exponent = attempt.saturating_sub(1).min(31);
360 let factor = self.multiplier.checked_pow(exponent).unwrap_or(u32::MAX);
361 self.initial_backoff
362 .saturating_mul(factor)
363 .min(self.max_backoff)
364 }
365
366 #[must_use]
368 pub const fn is_exhausted(self, attempt: u32) -> bool {
369 attempt >= self.max_attempts
370 }
371}
372
373impl Default for RetryPolicy {
374 fn default() -> Self {
375 Self::new(5)
376 }
377}
378
379#[derive(Clone, Debug, Default, Eq, PartialEq)]
381pub enum DeadLetter {
382 Topic(String),
384 #[default]
386 Discard,
387}
388
389#[derive(Clone, Debug, Eq, PartialEq)]
391pub enum WorkerStep {
392 Idle,
394 Completed {
396 receipt: String,
398 attempt: u32,
400 },
401 Retried {
403 receipt: String,
405 attempt: u32,
407 delay: Duration,
409 },
410 DeadLettered {
412 receipt: String,
414 attempt: u32,
416 reason: JobError,
418 },
419}
420
421pub struct Worker<Adapter, Handler> {
428 queue: Adapter,
429 topic: String,
430 handler: Handler,
431 retry: RetryPolicy,
432 dead_letter: DeadLetter,
433 idle_backoff: Duration,
434}
435
436impl<Adapter, Handler> Worker<Adapter, Handler> {
437 #[must_use]
439 pub fn new(queue: Adapter, topic: impl Into<String>, handler: Handler) -> Self {
440 Self {
441 queue,
442 topic: topic.into(),
443 handler,
444 retry: RetryPolicy::default(),
445 dead_letter: DeadLetter::Discard,
446 idle_backoff: Duration::from_millis(100),
447 }
448 }
449
450 #[must_use]
452 pub const fn with_retry(mut self, retry: RetryPolicy) -> Self {
453 self.retry = retry;
454 self
455 }
456
457 #[must_use]
459 pub fn with_dead_letter(mut self, dead_letter: DeadLetter) -> Self {
460 self.dead_letter = dead_letter;
461 self
462 }
463
464 #[must_use]
466 pub const fn with_idle_backoff(mut self, idle_backoff: Duration) -> Self {
467 self.idle_backoff = idle_backoff;
468 self
469 }
470
471 #[must_use]
473 pub fn topic(&self) -> &str {
474 &self.topic
475 }
476}
477
478impl<Adapter, Handler, HandlerFuture> Worker<Adapter, Handler>
479where
480 Adapter: Queue,
481 Handler: Fn(Delivery) -> HandlerFuture + Send + Sync,
482 HandlerFuture: Future<Output = Result<(), JobError>> + Send,
483{
484 pub async fn step(&self) -> Result<WorkerStep, QueueError> {
492 let Some(delivery) = self.queue.receive(&self.topic).await? else {
493 return Ok(WorkerStep::Idle);
494 };
495 let receipt = delivery.receipt.clone();
496 let attempt = delivery.attempt;
497 let message = delivery.message.clone();
498 let Err(reason) = (self.handler)(delivery).await else {
499 self.queue.ack(&receipt).await?;
500 return Ok(WorkerStep::Completed { receipt, attempt });
501 };
502 if reason.is_retryable() && !self.retry.is_exhausted(attempt) {
503 let delay = self.retry.backoff(attempt);
504 self.queue.nack(&receipt, delay).await?;
505 return Ok(WorkerStep::Retried {
506 receipt,
507 attempt,
508 delay,
509 });
510 }
511 self.route_dead_letter(&receipt, attempt, message, &reason)
512 .await?;
513 Ok(WorkerStep::DeadLettered {
514 receipt,
515 attempt,
516 reason,
517 })
518 }
519
520 pub async fn run<Sleep, SleepFuture>(&self, sleep: Sleep) -> Result<(), QueueError>
529 where
530 Sleep: Fn(Duration) -> SleepFuture,
531 SleepFuture: Future<Output = ()>,
532 {
533 loop {
534 if self.step().await? == WorkerStep::Idle {
535 sleep(self.idle_backoff).await;
536 }
537 }
538 }
539
540 async fn route_dead_letter(
541 &self,
542 receipt: &str,
543 attempt: u32,
544 message: Message,
545 reason: &JobError,
546 ) -> Result<(), QueueError> {
547 if let DeadLetter::Topic(destination) = &self.dead_letter {
548 let message = message
549 .with_header(DEAD_LETTER_SOURCE_HEADER, self.topic.clone())
550 .with_header(DEAD_LETTER_ATTEMPT_HEADER, attempt.to_string())
551 .with_header(DEAD_LETTER_REASON_HEADER, reason.message());
552 self.queue.publish(destination, message).await?;
553 }
554 self.queue.ack(receipt).await
555 }
556}
557
558#[derive(Clone, Debug, Eq, PartialEq)]
559pub enum QueueError {
560 Unavailable(String),
561 UnknownReceipt(String),
562}
563
564impl fmt::Display for QueueError {
565 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
566 match self {
567 Self::Unavailable(message) => write!(formatter, "queue unavailable: {message}"),
568 Self::UnknownReceipt(receipt) => write!(formatter, "unknown receipt `{receipt}`"),
569 }
570 }
571}
572
573impl std::error::Error for QueueError {}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use futures_lite::future;
579 use std::cell::Cell;
580
581 fn worker_queue() -> MemoryQueue {
582 let queue = MemoryQueue::default();
583 future::block_on(queue.publish("jobs", Message::new("one"))).expect("publish");
584 queue
585 }
586
587 #[test]
588 fn memory_adapter_redelivers_nacked_messages_after_the_delay() {
589 let queue = QueueClient::new(MemoryQueue::default());
590 future::block_on(queue.publish("jobs", Message::new("one"))).expect("publish");
591 let first = future::block_on(queue.receive("jobs"))
592 .expect("receive")
593 .expect("delivery");
594 future::block_on(queue.nack(&first.receipt, Duration::from_secs(5))).expect("nack");
595 assert!(
596 future::block_on(queue.receive("jobs"))
597 .expect("receive")
598 .is_none()
599 );
600 queue.adapter().advance(Duration::from_secs(5));
601 let second = future::block_on(queue.receive("jobs"))
602 .expect("receive")
603 .expect("redelivery");
604 assert_eq!(second.attempt, 2);
605 assert_eq!(second.message.body, b"one");
606 future::block_on(queue.ack(&second.receipt)).expect("ack");
607 assert_eq!(queue.adapter().in_flight(), 0);
608 }
609
610 #[test]
611 fn unknown_receipts_are_rejected() {
612 let queue = MemoryQueue::default();
613 assert_eq!(
614 future::block_on(queue.nack("missing", Duration::ZERO)),
615 Err(QueueError::UnknownReceipt("missing".to_owned()))
616 );
617 assert_eq!(
618 future::block_on(queue.ack("missing")),
619 Err(QueueError::UnknownReceipt("missing".to_owned()))
620 );
621 }
622
623 #[test]
624 fn worker_acknowledges_successful_deliveries() {
625 let queue = worker_queue();
626 let worker = Worker::new(queue.clone(), "jobs", |_delivery| async { Ok(()) });
627 let step = future::block_on(worker.step()).expect("step");
628 assert!(matches!(step, WorkerStep::Completed { attempt: 1, .. }));
629 assert_eq!(queue.depth("jobs"), 0);
630 assert_eq!(queue.in_flight(), 0);
631 assert_eq!(
632 future::block_on(worker.step()).expect("step"),
633 WorkerStep::Idle
634 );
635 }
636
637 #[test]
638 fn worker_retries_with_exponential_backoff_then_dead_letters() {
639 let queue = worker_queue();
640 let worker = Worker::new(queue.clone(), "jobs", |_delivery| async {
641 Err(JobError::retryable("boom"))
642 })
643 .with_retry(
644 RetryPolicy::new(3)
645 .with_initial_backoff(Duration::from_secs(1))
646 .with_max_backoff(Duration::from_secs(4)),
647 )
648 .with_dead_letter(DeadLetter::Topic("jobs.dead".to_owned()));
649
650 let first = future::block_on(worker.step()).expect("first attempt");
651 assert_eq!(
652 first,
653 WorkerStep::Retried {
654 receipt: "memory-1".to_owned(),
655 attempt: 1,
656 delay: Duration::from_secs(1),
657 }
658 );
659 assert_eq!(
660 future::block_on(worker.step()).expect("step"),
661 WorkerStep::Idle
662 );
663
664 queue.advance(Duration::from_secs(1));
665 let second = future::block_on(worker.step()).expect("second attempt");
666 assert_eq!(
667 second,
668 WorkerStep::Retried {
669 receipt: "memory-1".to_owned(),
670 attempt: 2,
671 delay: Duration::from_secs(2),
672 }
673 );
674
675 queue.advance(Duration::from_secs(2));
676 let third = future::block_on(worker.step()).expect("third attempt");
677 assert_eq!(
678 third,
679 WorkerStep::DeadLettered {
680 receipt: "memory-1".to_owned(),
681 attempt: 3,
682 reason: JobError::retryable("boom"),
683 }
684 );
685 assert_eq!(queue.depth("jobs"), 0);
686 assert_eq!(queue.in_flight(), 0);
687
688 let dead = future::block_on(queue.receive("jobs.dead"))
689 .expect("receive")
690 .expect("dead letter");
691 assert_eq!(dead.message.body, b"one");
692 assert_eq!(
693 dead.message.headers.get(DEAD_LETTER_SOURCE_HEADER),
694 Some(&"jobs".to_owned())
695 );
696 assert_eq!(
697 dead.message.headers.get(DEAD_LETTER_ATTEMPT_HEADER),
698 Some(&"3".to_owned())
699 );
700 assert_eq!(
701 dead.message.headers.get(DEAD_LETTER_REASON_HEADER),
702 Some(&"boom".to_owned())
703 );
704 }
705
706 #[test]
707 fn permanent_failures_skip_the_retry_budget() {
708 let queue = worker_queue();
709 let worker = Worker::new(queue.clone(), "jobs", |_delivery| async {
710 Err(JobError::permanent("poison"))
711 })
712 .with_retry(RetryPolicy::new(10))
713 .with_dead_letter(DeadLetter::Topic("jobs.dead".to_owned()));
714 let step = future::block_on(worker.step()).expect("step");
715 assert!(matches!(step, WorkerStep::DeadLettered { attempt: 1, .. }));
716 assert_eq!(queue.depth("jobs"), 0);
717 assert_eq!(queue.depth("jobs.dead"), 1);
718 }
719
720 #[test]
721 fn discarded_dead_letters_are_acknowledged() {
722 let queue = worker_queue();
723 let worker = Worker::new(queue.clone(), "jobs", |_delivery| async {
724 Err(JobError::permanent("poison"))
725 })
726 .with_retry(RetryPolicy::new(1));
727 assert_eq!(worker.topic(), "jobs");
728 future::block_on(worker.step()).expect("step");
729 assert_eq!(queue.depth("jobs"), 0);
730 assert_eq!(queue.in_flight(), 0);
731 }
732
733 #[derive(Clone, Default)]
734 struct FlakyQueue {
735 polls: Arc<Mutex<u32>>,
736 }
737
738 impl Queue for FlakyQueue {
739 fn publish(&self, _topic: &str, _message: Message) -> QueueFuture<()> {
740 Box::pin(async { Ok(()) })
741 }
742
743 fn receive(&self, _topic: &str) -> QueueFuture<Option<Delivery>> {
744 let polls = Arc::clone(&self.polls);
745 Box::pin(async move {
746 let mut polls = polls
747 .lock()
748 .unwrap_or_else(std::sync::PoisonError::into_inner);
749 *polls += 1;
750 if *polls > 2 {
751 return Err(QueueError::Unavailable("closed".to_owned()));
752 }
753 Ok(None)
754 })
755 }
756
757 fn ack(&self, _receipt: &str) -> QueueFuture<()> {
758 Box::pin(async { Ok(()) })
759 }
760
761 fn nack(&self, _receipt: &str, _delay: Duration) -> QueueFuture<()> {
762 Box::pin(async { Ok(()) })
763 }
764 }
765
766 #[test]
767 fn run_idles_between_empty_polls_and_reports_adapter_failures() {
768 let worker = Worker::new(FlakyQueue::default(), "jobs", |_delivery| async { Ok(()) })
769 .with_idle_backoff(Duration::from_millis(5));
770 let idles = Cell::new(0_u32);
771 let error = future::block_on(worker.run(|delay| {
772 assert_eq!(delay, Duration::from_millis(5));
773 idles.set(idles.get() + 1);
774 async {}
775 }))
776 .expect_err("adapter failure");
777 assert_eq!(error, QueueError::Unavailable("closed".to_owned()));
778 assert_eq!(idles.get(), 2);
779 }
780
781 #[test]
782 fn backoff_grows_and_saturates() {
783 let policy = RetryPolicy::new(4)
784 .with_initial_backoff(Duration::from_millis(100))
785 .with_max_backoff(Duration::from_secs(1))
786 .with_multiplier(3);
787 assert_eq!(policy.backoff(1), Duration::from_millis(100));
788 assert_eq!(policy.backoff(2), Duration::from_millis(300));
789 assert_eq!(policy.backoff(3), Duration::from_millis(900));
790 assert_eq!(policy.backoff(4), Duration::from_secs(1));
791 assert_eq!(policy.backoff(u32::MAX), Duration::from_secs(1));
792 assert_eq!(policy.max_attempts(), 4);
793 assert!(policy.is_exhausted(4));
794 assert!(!policy.is_exhausted(3));
795 assert_eq!(RetryPolicy::new(0).max_attempts(), 1);
796 assert_eq!(
797 RetryPolicy::new(2).with_multiplier(0).backoff(3),
798 Duration::from_secs(1)
799 );
800 }
801}