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 match consumer.receive().await {
50 Ok(received) => {
51 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}