mq_bridge/endpoints/
fanout.rs1use 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 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 let batch_sends = self.publishers.iter().map(|p| {
39 p.send_batch(messages.clone())
41 });
42
43 let results = join_all(batch_sends).await;
44
45 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}