Skip to main content

mq_bridge/endpoints/
reader.rs

1use crate::traits::{
2    BoxFuture, ConsumerError, MessageConsumer, MessageDisposition, MessagePublisher,
3    PublisherError, Sent, SentBatch,
4};
5use crate::CanonicalMessage;
6use async_trait::async_trait;
7use std::any::Any;
8use std::sync::Arc;
9use tokio::sync::Mutex;
10
11pub struct ReaderPublisher {
12    consumer: Arc<Mutex<Box<dyn MessageConsumer>>>,
13}
14
15impl ReaderPublisher {
16    pub fn new(consumer: Box<dyn MessageConsumer>) -> Self {
17        Self {
18            consumer: Arc::new(Mutex::new(consumer)),
19        }
20    }
21}
22
23#[async_trait]
24impl MessagePublisher for ReaderPublisher {
25    fn on_connect_hook(&self) -> Option<BoxFuture<'_, anyhow::Result<()>>> {
26        Some(Box::pin(async move {
27            let consumer = self.consumer.lock().await;
28            if let Some(hook) = consumer.on_connect_hook() {
29                hook.await?;
30            }
31            Ok(())
32        }))
33    }
34
35    fn on_disconnect_hook(&self) -> Option<BoxFuture<'_, anyhow::Result<()>>> {
36        Some(Box::pin(async move {
37            let consumer = self.consumer.lock().await;
38            if let Some(hook) = consumer.on_disconnect_hook() {
39                hook.await?;
40            }
41            Ok(())
42        }))
43    }
44
45    async fn send(&self, _message: CanonicalMessage) -> Result<Sent, PublisherError> {
46        let mut consumer = self.consumer.lock().await;
47        // We ignore the incoming message payload and just read from the consumer.
48        // The incoming message acts purely as a trigger.
49        match consumer.receive().await {
50            Ok(received) => {
51                // We must commit the message immediately because the Publisher interface
52                // doesn't support passing the commit responsibility back to the caller
53                // in a way that aligns with the input's commit lifecycle.
54                if let Err(e) = (received.commit)(MessageDisposition::Ack).await {
55                    return Err(PublisherError::Retryable(anyhow::anyhow!(
56                        "Failed to commit message in ReaderPublisher: {}",
57                        e
58                    )));
59                }
60                Ok(Sent::Response(received.message))
61            }
62            Err(e) => match e {
63                ConsumerError::EndOfStream => Err(PublisherError::NonRetryable(anyhow::anyhow!(e))),
64                _ => Err(PublisherError::Retryable(anyhow::anyhow!(e))),
65            },
66        }
67    }
68
69    async fn send_batch(
70        &self,
71        messages: Vec<CanonicalMessage>,
72    ) -> Result<SentBatch, PublisherError> {
73        let count = messages.len();
74        if count == 0 {
75            return Ok(SentBatch::Ack);
76        }
77
78        let mut consumer = self.consumer.lock().await;
79        match consumer.receive_batch(count).await {
80            Ok(batch) => {
81                let received_count = batch.messages.len();
82                if received_count > 0 {
83                    if let Err(e) =
84                        (batch.commit)(vec![MessageDisposition::Ack; received_count]).await
85                    {
86                        return Err(PublisherError::Retryable(anyhow::anyhow!(
87                            "Failed to commit batch in ReaderPublisher: {}",
88                            e
89                        )));
90                    }
91                }
92
93                Ok(SentBatch::Ack)
94            }
95            Err(e) => match e {
96                ConsumerError::EndOfStream => Err(PublisherError::NonRetryable(anyhow::anyhow!(e))),
97                _ => Err(PublisherError::Retryable(anyhow::anyhow!(e))),
98            },
99        }
100    }
101
102    fn as_any(&self) -> &dyn Any {
103        self
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::outcomes::{Received, ReceivedBatch};
111    use crate::traits::{BatchCommitFunc, CommitFunc, EndpointStatus};
112    use std::sync::atomic::{AtomicUsize, Ordering};
113    use std::sync::{Arc as StdArc, Mutex as StdMutex};
114
115    struct MockConsumer {
116        single_result: Option<Result<CanonicalMessage, ConsumerError>>,
117        batch_result: Option<Result<Vec<CanonicalMessage>, ConsumerError>>,
118        commit_log: StdArc<StdMutex<Vec<Vec<MessageDisposition>>>>,
119        commit_error: Option<String>,
120        connect_calls: StdArc<AtomicUsize>,
121        disconnect_calls: StdArc<AtomicUsize>,
122    }
123
124    impl MockConsumer {
125        fn new_single(
126            result: Result<CanonicalMessage, ConsumerError>,
127            commit_log: StdArc<StdMutex<Vec<Vec<MessageDisposition>>>>,
128        ) -> Self {
129            Self {
130                single_result: Some(result),
131                batch_result: None,
132                commit_log,
133                commit_error: None,
134                connect_calls: StdArc::new(AtomicUsize::new(0)),
135                disconnect_calls: StdArc::new(AtomicUsize::new(0)),
136            }
137        }
138
139        fn new_batch(
140            result: Result<Vec<CanonicalMessage>, ConsumerError>,
141            commit_log: StdArc<StdMutex<Vec<Vec<MessageDisposition>>>>,
142        ) -> Self {
143            Self {
144                single_result: None,
145                batch_result: Some(result),
146                commit_log,
147                commit_error: None,
148                connect_calls: StdArc::new(AtomicUsize::new(0)),
149                disconnect_calls: StdArc::new(AtomicUsize::new(0)),
150            }
151        }
152
153        fn with_commit_error(mut self, message: &str) -> Self {
154            self.commit_error = Some(message.to_string());
155            self
156        }
157
158        fn commit_func(&self) -> CommitFunc {
159            let log = self.commit_log.clone();
160            let error = self.commit_error.clone();
161            Box::new(move |disposition| {
162                let log = log.clone();
163                let error = error.clone();
164                Box::pin(async move {
165                    log.lock().unwrap().push(vec![disposition]);
166                    if let Some(message) = error {
167                        Err(anyhow::anyhow!(message))
168                    } else {
169                        Ok(())
170                    }
171                })
172            })
173        }
174
175        fn batch_commit_func(&self) -> BatchCommitFunc {
176            let log = self.commit_log.clone();
177            let error = self.commit_error.clone();
178            Box::new(move |dispositions| {
179                let log = log.clone();
180                let error = error.clone();
181                Box::pin(async move {
182                    log.lock().unwrap().push(dispositions);
183                    if let Some(message) = error {
184                        Err(anyhow::anyhow!(message))
185                    } else {
186                        Ok(())
187                    }
188                })
189            })
190        }
191    }
192
193    #[async_trait]
194    impl MessageConsumer for MockConsumer {
195        fn on_connect_hook(&self) -> Option<BoxFuture<'_, anyhow::Result<()>>> {
196            let calls = self.connect_calls.clone();
197            Some(Box::pin(async move {
198                calls.fetch_add(1, Ordering::SeqCst);
199                Ok(())
200            }))
201        }
202
203        fn on_disconnect_hook(&self) -> Option<BoxFuture<'_, anyhow::Result<()>>> {
204            let calls = self.disconnect_calls.clone();
205            Some(Box::pin(async move {
206                calls.fetch_add(1, Ordering::SeqCst);
207                Ok(())
208            }))
209        }
210
211        async fn receive(&mut self) -> Result<Received, ConsumerError> {
212            match self
213                .single_result
214                .take()
215                .expect("single_result should be configured for this test")
216            {
217                Ok(message) => Ok(Received {
218                    message,
219                    commit: self.commit_func(),
220                }),
221                Err(err) => Err(err),
222            }
223        }
224
225        async fn receive_batch(
226            &mut self,
227            _max_messages: usize,
228        ) -> Result<ReceivedBatch, ConsumerError> {
229            match self
230                .batch_result
231                .take()
232                .expect("batch_result should be configured for this test")
233            {
234                Ok(messages) => Ok(ReceivedBatch {
235                    messages,
236                    commit: self.batch_commit_func(),
237                }),
238                Err(err) => Err(err),
239            }
240        }
241
242        async fn status(&self) -> EndpointStatus {
243            EndpointStatus::default()
244        }
245
246        fn as_any(&self) -> &dyn Any {
247            self
248        }
249    }
250
251    #[tokio::test]
252    async fn test_reader_publisher_send_returns_response_and_commits_ack() {
253        let commit_log = StdArc::new(StdMutex::new(Vec::new()));
254        let publisher = ReaderPublisher::new(Box::new(MockConsumer::new_single(
255            Ok(CanonicalMessage::from("from-reader")),
256            commit_log.clone(),
257        )));
258
259        let sent = publisher
260            .send(CanonicalMessage::from("trigger"))
261            .await
262            .unwrap();
263        match sent {
264            Sent::Response(message) => assert_eq!(message.get_payload_str(), "from-reader"),
265            Sent::Ack => panic!("expected response"),
266        }
267
268        assert_eq!(commit_log.lock().unwrap().len(), 1);
269        assert!(matches!(
270            commit_log.lock().unwrap()[0].as_slice(),
271            [MessageDisposition::Ack]
272        ));
273    }
274
275    #[tokio::test]
276    async fn test_reader_publisher_send_maps_end_of_stream_to_non_retryable_error() {
277        let publisher = ReaderPublisher::new(Box::new(MockConsumer::new_single(
278            Err(ConsumerError::EndOfStream),
279            StdArc::new(StdMutex::new(Vec::new())),
280        )));
281
282        let err = publisher
283            .send(CanonicalMessage::from("trigger"))
284            .await
285            .unwrap_err();
286        assert!(matches!(err, PublisherError::NonRetryable(_)));
287    }
288
289    #[tokio::test]
290    async fn test_reader_publisher_send_batch_commits_all_received_messages() {
291        let commit_log = StdArc::new(StdMutex::new(Vec::new()));
292        let publisher = ReaderPublisher::new(Box::new(MockConsumer::new_batch(
293            Ok(vec![
294                CanonicalMessage::from("one"),
295                CanonicalMessage::from("two"),
296            ]),
297            commit_log.clone(),
298        )));
299
300        let sent = publisher
301            .send_batch(vec![
302                CanonicalMessage::from("trigger-1"),
303                CanonicalMessage::from("trigger-2"),
304            ])
305            .await
306            .unwrap();
307        assert!(matches!(sent, SentBatch::Ack));
308        assert_eq!(commit_log.lock().unwrap().len(), 1);
309        assert!(commit_log.lock().unwrap()[0]
310            .iter()
311            .all(|disposition| matches!(disposition, MessageDisposition::Ack)));
312    }
313
314    #[tokio::test]
315    async fn test_reader_publisher_send_batch_commit_failure_is_retryable() {
316        let publisher = ReaderPublisher::new(Box::new(
317            MockConsumer::new_batch(
318                Ok(vec![CanonicalMessage::from("one")]),
319                StdArc::new(StdMutex::new(Vec::new())),
320            )
321            .with_commit_error("commit failed"),
322        ));
323
324        let err = publisher
325            .send_batch(vec![CanonicalMessage::from("trigger")])
326            .await
327            .unwrap_err();
328        assert!(matches!(err, PublisherError::Retryable(_)));
329    }
330
331    #[tokio::test]
332    async fn test_reader_publisher_runs_consumer_hooks() {
333        let consumer =
334            MockConsumer::new_batch(Ok(Vec::new()), StdArc::new(StdMutex::new(Vec::new())));
335        let connect_calls = consumer.connect_calls.clone();
336        let disconnect_calls = consumer.disconnect_calls.clone();
337        let publisher = ReaderPublisher::new(Box::new(consumer));
338
339        publisher
340            .on_connect_hook()
341            .unwrap()
342            .await
343            .expect("connect hook should succeed");
344        publisher
345            .on_disconnect_hook()
346            .unwrap()
347            .await
348            .expect("disconnect hook should succeed");
349
350        assert_eq!(connect_calls.load(Ordering::SeqCst), 1);
351        assert_eq!(disconnect_calls.load(Ordering::SeqCst), 1);
352        assert!(publisher.as_any().is::<ReaderPublisher>());
353    }
354}