Skip to main content

mq_bridge/endpoints/
fanout.rs

1use crate::traits::{EndpointStatus, MessagePublisher, PublisherError, Sent, SentBatch};
2use crate::CanonicalMessage;
3use async_trait::async_trait;
4use std::any::Any;
5use std::sync::Arc;
6
7pub struct FanoutPublisher {
8    publishers: Vec<Arc<dyn MessagePublisher>>,
9}
10
11impl FanoutPublisher {
12    pub fn new(publishers: Vec<Arc<dyn MessagePublisher>>) -> Self {
13        Self { publishers }
14    }
15}
16
17#[async_trait]
18impl MessagePublisher for FanoutPublisher {
19    async fn send(&self, message: CanonicalMessage) -> Result<Sent, PublisherError> {
20        for publisher in &self.publishers {
21            // We must clone the message for each publisher.
22            publisher.send(message.clone()).await?;
23        }
24        Ok(Sent::Ack)
25    }
26
27    async fn send_batch(
28        &self,
29        messages: Vec<CanonicalMessage>,
30    ) -> Result<SentBatch, PublisherError> {
31        use futures::future::join_all;
32
33        if messages.is_empty() {
34            return Ok(SentBatch::Ack);
35        }
36
37        // Send the batch to all publishers concurrently.
38        let batch_sends = self.publishers.iter().map(|p| {
39            // Each publisher gets a clone of the entire batch. This can be memory-intensive.
40            p.send_batch(messages.clone())
41        });
42
43        let results = join_all(batch_sends).await;
44
45        // For fan-out, we consider the batch successful if it was successfully sent to *all* publishers.
46        // If any publisher returns a hard error, we propagate it.
47        // We don't currently aggregate partial failures from different fan-out destinations.
48        for result in results {
49            result?;
50        }
51
52        Ok(SentBatch::Ack)
53    }
54
55    async fn status(&self) -> EndpointStatus {
56        use futures::future::join_all;
57
58        let status_futs = self.publishers.iter().map(|p| p.status());
59        let results = join_all(status_futs).await;
60
61        let mut healthy = true;
62        let mut pending = 0;
63        let mut capacity = 0;
64        let mut error: Option<String> = None;
65        let mut details = Vec::new();
66
67        for status in results {
68            if !status.healthy {
69                healthy = false;
70                if error.is_none() {
71                    error = status.error.clone();
72                }
73            }
74            pending += status.pending.unwrap_or(0);
75            capacity += status.capacity.unwrap_or(0);
76            details.push(status);
77        }
78
79        EndpointStatus {
80            healthy,
81            pending: Some(pending),
82            capacity: Some(capacity),
83            error,
84            details: serde_json::json!({ "destinations": details }),
85            ..Default::default()
86        }
87    }
88
89    fn as_any(&self) -> &dyn Any {
90        self
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::errors::ProcessingError;
98    use async_trait::async_trait;
99    use std::sync::Mutex;
100
101    #[derive(Default)]
102    struct RecordingPublisher {
103        single_payloads: Mutex<Vec<String>>,
104        batch_payloads: Mutex<Vec<Vec<String>>>,
105        status: EndpointStatus,
106        batch_error: Option<String>,
107    }
108
109    #[async_trait]
110    impl MessagePublisher for RecordingPublisher {
111        async fn send(&self, message: CanonicalMessage) -> Result<Sent, PublisherError> {
112            self.single_payloads
113                .lock()
114                .unwrap()
115                .push(message.get_payload_str().to_string());
116            Ok(Sent::Ack)
117        }
118
119        async fn send_batch(
120            &self,
121            messages: Vec<CanonicalMessage>,
122        ) -> Result<SentBatch, PublisherError> {
123            self.batch_payloads.lock().unwrap().push(
124                messages
125                    .iter()
126                    .map(|message| message.get_payload_str().to_string())
127                    .collect(),
128            );
129
130            if let Some(message) = &self.batch_error {
131                return Err(ProcessingError::NonRetryable(anyhow::anyhow!(
132                    message.clone()
133                )));
134            }
135
136            Ok(SentBatch::Ack)
137        }
138
139        async fn status(&self) -> EndpointStatus {
140            self.status.clone()
141        }
142
143        fn as_any(&self) -> &dyn Any {
144            self
145        }
146    }
147
148    #[tokio::test]
149    async fn test_fanout_send_delivers_message_to_all_publishers() {
150        let left = Arc::new(RecordingPublisher::default());
151        let right = Arc::new(RecordingPublisher::default());
152        let fanout = FanoutPublisher::new(vec![left.clone(), right.clone()]);
153
154        let result = fanout.send(CanonicalMessage::from("hello")).await.unwrap();
155        assert!(matches!(result, Sent::Ack));
156        assert_eq!(left.single_payloads.lock().unwrap().as_slice(), ["hello"]);
157        assert_eq!(right.single_payloads.lock().unwrap().as_slice(), ["hello"]);
158    }
159
160    #[tokio::test]
161    async fn test_fanout_send_batch_propagates_errors() {
162        let ok = Arc::new(RecordingPublisher::default());
163        let failing = Arc::new(RecordingPublisher {
164            batch_error: Some("fanout failure".to_string()),
165            ..Default::default()
166        });
167        let fanout = FanoutPublisher::new(vec![ok.clone(), failing.clone()]);
168
169        let err = fanout
170            .send_batch(vec![
171                CanonicalMessage::from("one"),
172                CanonicalMessage::from("two"),
173            ])
174            .await
175            .unwrap_err();
176        assert!(matches!(err, ProcessingError::NonRetryable(_)));
177        assert_eq!(ok.batch_payloads.lock().unwrap().len(), 1);
178        assert_eq!(failing.batch_payloads.lock().unwrap().len(), 1);
179    }
180
181    #[tokio::test]
182    async fn test_fanout_status_aggregates_destination_status() {
183        let healthy = Arc::new(RecordingPublisher {
184            status: EndpointStatus {
185                healthy: true,
186                target: "a".to_string(),
187                pending: Some(2),
188                capacity: Some(5),
189                error: None,
190                details: serde_json::json!({"id": "a"}),
191            },
192            ..Default::default()
193        });
194        let unhealthy = Arc::new(RecordingPublisher {
195            status: EndpointStatus {
196                healthy: false,
197                target: "b".to_string(),
198                pending: Some(3),
199                capacity: Some(7),
200                error: Some("down".to_string()),
201                details: serde_json::json!({"id": "b"}),
202            },
203            ..Default::default()
204        });
205        let fanout = FanoutPublisher::new(vec![healthy, unhealthy]);
206
207        let status = fanout.status().await;
208        assert!(!status.healthy);
209        assert_eq!(status.pending, Some(5));
210        assert_eq!(status.capacity, Some(12));
211        assert_eq!(status.error.as_deref(), Some("down"));
212        assert_eq!(status.details["destinations"].as_array().unwrap().len(), 2);
213    }
214}