Skip to main content

mocra_core/queue/
lib.rs

1pub mod batcher;
2pub mod channel;
3pub mod compensation;
4pub mod compression;
5#[cfg(feature = "queue-kafka")]
6pub mod kafka;
7pub mod manager;
8#[cfg(feature = "queue-nats")]
9pub mod nats;
10
11use crate::errors::Result;
12pub use crate::queue::channel::Channel;
13use async_trait::async_trait;
14pub use compensation::{Compensator, Identifiable};
15pub use manager::QueueManager;
16use tokio::sync::mpsc;
17
18#[cfg(test)]
19mod tests;
20
21#[derive(Debug, Clone)]
22/// Action to take on a message: Acknowledge or Negative Acknowledge (Fail)
23pub enum AckAction {
24    /// Message processed successfully
25    Ack,
26    /// Message processing failed
27    Nack(
28        String,
29        std::sync::Arc<Vec<u8>>,
30        std::sync::Arc<HashMap<String, String>>,
31    ), // reason, payload, headers
32}
33
34use futures::future::BoxFuture;
35use std::collections::HashMap;
36
37pub type AckFn = Box<dyn FnOnce() -> BoxFuture<'static, Result<()>> + Send + Sync>;
38pub type NackFn = Box<dyn FnOnce(String) -> BoxFuture<'static, Result<()>> + Send + Sync>;
39
40pub const HEADER_ATTEMPT: &str = "x-attempt";
41pub const HEADER_CREATED_AT: &str = "x-created-at";
42pub const HEADER_NACK_REASON: &str = "x-nack-reason";
43
44#[derive(Debug, Clone, Copy, Default)]
45pub struct NackPolicy {
46    pub max_retries: u32,
47    pub backoff_ms: u64,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum NackDisposition {
52    Retry { next_attempt: u32 },
53    Dlq,
54}
55
56#[allow(dead_code)] // used by queue-kafka / queue-nats backends
57pub(crate) fn parse_attempt(headers: &HashMap<String, String>) -> u32 {
58    headers
59        .get(HEADER_ATTEMPT)
60        .and_then(|v| v.parse::<u32>().ok())
61        .unwrap_or(0)
62}
63
64pub fn decide_nack(policy: NackPolicy, attempt: u32) -> NackDisposition {
65    if policy.max_retries > 0 && attempt < policy.max_retries {
66        NackDisposition::Retry {
67            next_attempt: attempt.saturating_add(1),
68        }
69    } else {
70        NackDisposition::Dlq
71    }
72}
73
74/// A wrapper for items that might require acknowledgement
75pub struct QueuedItem<T> {
76    pub inner: T,
77    ack_fn: Option<AckFn>,
78    nack_fn: Option<NackFn>,
79}
80
81impl<T> QueuedItem<T> {
82    pub fn new(inner: T) -> Self {
83        Self {
84            inner,
85            ack_fn: None,
86            nack_fn: None,
87        }
88    }
89
90    pub fn with_ack<A, N>(inner: T, ack: A, nack: N) -> Self
91    where
92        A: FnOnce() -> BoxFuture<'static, Result<()>> + Send + Sync + 'static,
93        N: FnOnce(String) -> BoxFuture<'static, Result<()>> + Send + Sync + 'static,
94    {
95        Self {
96            inner,
97            ack_fn: Some(Box::new(ack)),
98            nack_fn: Some(Box::new(nack)),
99        }
100    }
101
102    pub async fn ack(mut self) -> Result<()> {
103        if let Some(f) = self.ack_fn.take() {
104            f().await
105        } else {
106            Ok(())
107        }
108    }
109
110    pub async fn nack(mut self, reason: String) -> Result<()> {
111        if let Some(f) = self.nack_fn.take() {
112            f(reason).await
113        } else {
114            Ok(())
115        }
116    }
117
118    pub fn into_parts(self) -> (T, Option<AckFn>, Option<NackFn>) {
119        (self.inner, self.ack_fn, self.nack_fn)
120    }
121}
122
123impl<T> std::ops::Deref for QueuedItem<T> {
124    type Target = T;
125    fn deref(&self) -> &Self::Target {
126        &self.inner
127    }
128}
129
130impl<T: std::fmt::Debug> std::fmt::Debug for QueuedItem<T> {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        f.debug_struct("QueuedItem")
133            .field("inner", &self.inner)
134            .finish()
135    }
136}
137
138impl<T: Identifiable> Identifiable for QueuedItem<T> {
139    fn get_id(&self) -> String {
140        self.inner.get_id()
141    }
142
143    fn partition_key(&self) -> String {
144        self.inner.partition_key()
145    }
146}
147
148/// Represents a message received from the queue.
149/// The user MUST call `ack()` to acknowledge successful processing.
150#[derive(Clone)]
151pub struct Message {
152    pub payload: std::sync::Arc<Vec<u8>>,
153    pub id: String,
154    pub headers: std::sync::Arc<HashMap<String, String>>,
155    pub ack_tx: mpsc::Sender<(String, AckAction)>,
156}
157
158impl std::fmt::Debug for Message {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.debug_struct("Message")
161            .field("id", &self.id)
162            .field("payload", &self.payload)
163            .field("headers", &self.headers)
164            .finish()
165    }
166}
167
168impl Message {
169    pub async fn ack(&self) -> Result<()> {
170        // Send the ID back to the backend to acknowledge the message
171        self.ack_tx
172            .send((self.id.clone(), AckAction::Ack))
173            .await
174            .map_err(|_| {
175                crate::errors::error::QueueError::OperationFailed(Box::new(std::io::Error::other(
176                    "Failed to send ACK signal",
177                )))
178                .into()
179            })
180    }
181
182    pub async fn nack(&self, reason: impl Into<String>) -> Result<()> {
183        let headers = self.headers.clone();
184        self.ack_tx
185            .send((
186                self.id.clone(),
187                AckAction::Nack(reason.into(), self.payload.clone(), headers),
188            ))
189            .await
190            .map_err(|_| {
191                crate::errors::error::QueueError::OperationFailed(Box::new(std::io::Error::other(
192                    "Failed to send NACK signal",
193                )))
194                .into()
195            })
196    }
197}
198
199#[async_trait]
200pub trait MqBackend: Send + Sync {
201    async fn publish(&self, topic: &str, key: Option<&str>, payload: &[u8]) -> Result<()> {
202        self.publish_with_headers(topic, key, payload, &HashMap::new())
203            .await
204    }
205
206    async fn publish_with_headers(
207        &self,
208        topic: &str,
209        key: Option<&str>,
210        payload: &[u8],
211        headers: &HashMap<String, String>,
212    ) -> Result<()>;
213
214    async fn publish_batch(&self, topic: &str, items: &[(Option<String>, Vec<u8>)]) -> Result<()> {
215        for (key, payload) in items {
216            self.publish(topic, key.as_deref(), payload).await?;
217        }
218        Ok(())
219    }
220
221    async fn publish_batch_with_headers(
222        &self,
223        topic: &str,
224        items: &[(Option<String>, Vec<u8>, HashMap<String, String>)],
225    ) -> Result<()> {
226        for (key, payload, headers) in items {
227            self.publish_with_headers(topic, key.as_deref(), payload, headers)
228                .await?;
229        }
230        Ok(())
231    }
232
233    async fn subscribe(&self, topic: &str, sender: mpsc::Sender<Message>) -> Result<()>;
234    async fn clean_storage(&self) -> Result<()>;
235    /// Send a message to the Dead Letter Queue (DLQ).
236    async fn send_to_dlq(&self, topic: &str, id: &str, payload: &[u8], reason: &str) -> Result<()>;
237
238    /// Read messages from the Dead Letter Queue (DLQ).
239    /// Returns a list of (id, payload, reason, original_id).
240    async fn read_dlq(
241        &self,
242        topic: &str,
243        count: usize,
244    ) -> Result<Vec<(String, Vec<u8>, String, String)>>;
245}