camel_processor/
multicast_segment.rs1use futures::FutureExt;
7use std::future::Future;
8use std::panic::AssertUnwindSafe;
9use std::pin::Pin;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
12use std::time::Duration;
13use tokio::task::JoinSet;
14
15use camel_api::{Exchange, Value};
16
17pub const CAMEL_MULTICAST_INDEX: &str = "CamelMulticastIndex";
21pub const CAMEL_MULTICAST_COMPLETE: &str = "CamelMulticastComplete";
23
24#[derive(Clone)]
38pub struct MulticastSegment {
39 pub branches: Vec<camel_api::OutcomeSegment>,
40 pub parallel: bool,
41 pub parallel_limit: Option<usize>,
43 pub stop_on_exception: bool,
54 pub timeout: Option<Duration>,
56 pub aggregator: Arc<dyn Fn(Vec<Exchange>) -> Exchange + Send + Sync>,
57}
58
59impl camel_api::OutcomePipeline for MulticastSegment {
60 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
61 Box::new(self.clone())
62 }
63
64 fn run<'a>(
65 &'a mut self,
66 exchange: Exchange,
67 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
68 Box::pin(async move {
69 if self.parallel {
70 parallel_multicast(self, exchange).await
71 } else {
72 sequential_multicast(self, exchange).await
73 }
74 })
75 }
76}
77
78async fn sequential_multicast(
81 seg: &mut MulticastSegment,
82 exchange: Exchange,
83) -> camel_api::PipelineOutcome {
84 let mut outputs = Vec::new();
85 let mut last_error: Option<camel_api::CamelError> = None;
86 let total = seg.branches.len();
87 for (i, branch) in seg.branches.iter_mut().enumerate() {
88 let mut ex = exchange.branch_clone();
90 ex.set_property(CAMEL_MULTICAST_INDEX, Value::from(i as i64));
91 ex.set_property(CAMEL_MULTICAST_COMPLETE, Value::Bool(i == total - 1));
92 match branch.run(ex).await {
93 camel_api::PipelineOutcome::Completed(ex) => outputs.push(ex),
94 camel_api::PipelineOutcome::Stopped(ex) => {
95 return camel_api::PipelineOutcome::Stopped(ex);
96 }
97 camel_api::PipelineOutcome::Failed(err) => {
98 if seg.stop_on_exception {
99 return camel_api::PipelineOutcome::Failed(err);
100 }
101 last_error = Some(err);
103 }
104 }
105 }
106 if let Some(err) = last_error {
107 if outputs.is_empty() {
108 return camel_api::PipelineOutcome::Failed(err);
109 }
110 tracing::warn!(
112 failed_branches = total - outputs.len(),
113 branch_count = total,
114 "multicast partial success: discarding failed branch outcomes"
115 );
116 }
117 camel_api::PipelineOutcome::Completed((seg.aggregator)(outputs))
118}
119
120async fn parallel_multicast(
127 seg: &mut MulticastSegment,
128 exchange: Exchange,
129) -> camel_api::PipelineOutcome {
130 use std::sync::Arc;
131 use tokio::sync::Semaphore;
132
133 let stopped_seen = Arc::new(AtomicBool::new(false));
134 let stopped_idx = Arc::new(AtomicUsize::new(usize::MAX));
135 let semaphore = seg
136 .parallel_limit
137 .filter(|&limit| limit > 0)
138 .map(|limit| Arc::new(Semaphore::new(limit)));
139 let timeout = seg.timeout;
140 let stop_on_exception = seg.stop_on_exception;
141 let total = seg.branches.len();
142
143 let mut set: JoinSet<(usize, Option<camel_api::PipelineOutcome>)> = JoinSet::new();
144
145 for (idx, mut branch) in seg.branches.clone().into_iter().enumerate() {
146 let stopped_seen = Arc::clone(&stopped_seen);
147 let stopped_idx = Arc::clone(&stopped_idx);
148 let sem = semaphore.clone();
149 let mut ex = exchange.branch_clone();
151 ex.set_property(CAMEL_MULTICAST_INDEX, Value::from(idx as i64));
152 ex.set_property(CAMEL_MULTICAST_COMPLETE, Value::Bool(idx == total - 1));
153 set.spawn(async move {
154 if stopped_seen.load(Ordering::SeqCst) {
156 return (idx, None);
157 }
158 let _permit: Option<tokio::sync::OwnedSemaphorePermit> = match &sem {
160 Some(s) => match Arc::clone(s).acquire_owned().await {
161 Ok(p) => Some(p),
162 Err(_) => {
163 return (
164 idx,
165 Some(camel_api::PipelineOutcome::Failed(
166 camel_api::CamelError::ProcessorError("semaphore closed".into()),
167 )),
168 );
169 }
170 },
171 None => None,
172 };
173 if stopped_seen.load(Ordering::SeqCst) {
175 return (idx, None);
176 }
177
178 let outcome = async {
181 let outcome = branch.run(ex).await;
182 if let camel_api::PipelineOutcome::Stopped(_) = &outcome {
183 loop {
185 let cur = stopped_idx.load(Ordering::SeqCst);
186 if idx >= cur {
187 break;
188 }
189 match stopped_idx.compare_exchange_weak(
190 cur,
191 idx,
192 Ordering::SeqCst,
193 Ordering::SeqCst,
194 ) {
195 Ok(_) => break,
196 Err(actual) => {
197 if actual <= idx {
198 break;
199 }
200 }
201 }
202 }
203 stopped_seen.store(true, Ordering::SeqCst);
204 }
205 outcome
206 };
207
208 let outcome = AssertUnwindSafe(outcome).catch_unwind();
218 let outcome = if let Some(dur) = timeout {
219 match tokio::time::timeout(dur, outcome).await {
220 Ok(Ok(o)) => o,
221 Ok(Err(panic_payload)) => {
222 let failure = camel_api::PipelineOutcome::Failed(
223 camel_api::CamelError::ProcessorError(format!(
224 "multicast branch {idx} panicked"
225 )),
226 );
227 std::mem::forget(panic_payload);
228 failure
229 }
230 Err(_elapsed) => {
231 camel_api::PipelineOutcome::Failed(camel_api::CamelError::ProcessorError(
232 format!("multicast branch {idx} timed out after {dur:?}"),
233 ))
234 }
235 }
236 } else {
237 match outcome.await {
238 Ok(outcome) => outcome,
239 Err(panic_payload) => {
240 let failure = camel_api::PipelineOutcome::Failed(
241 camel_api::CamelError::ProcessorError(format!(
242 "multicast branch {idx} panicked"
243 )),
244 );
245 std::mem::forget(panic_payload);
246 failure
247 }
248 }
249 };
250
251 (idx, Some(outcome))
252 });
253 }
254
255 let mut results: Vec<(usize, camel_api::PipelineOutcome)> = Vec::new();
257 while let Some(res) = set.join_next().await {
258 if let Ok((idx, Some(o))) = res {
259 results.push((idx, o));
260 }
261 }
262
263 if stopped_seen.load(Ordering::SeqCst) {
265 let winning_idx = stopped_idx.load(Ordering::SeqCst);
266 if winning_idx == usize::MAX {
267 tracing::warn!(
268 target: "camel.phase4.multicast",
269 "stopped_seen=true but stopped_idx=usize::MAX — race; falling back to pre-multicast exchange"
270 );
271 return camel_api::PipelineOutcome::Stopped(exchange);
272 }
273 let stopped_ex = results
274 .iter()
275 .find(|(idx, _)| *idx == winning_idx)
276 .and_then(|(_, o)| match o {
277 camel_api::PipelineOutcome::Stopped(ex) => Some(ex.branch_clone()),
278 _ => None,
279 });
280 if let Some(ex) = stopped_ex {
281 return camel_api::PipelineOutcome::Stopped(ex);
282 }
283 tracing::warn!(
284 target: "camel.phase4.multicast",
285 winning_idx = winning_idx,
286 "winning_idx not found — falling back to pre-multicast exchange"
287 );
288 return camel_api::PipelineOutcome::Stopped(exchange);
289 }
290
291 results.sort_by_key(|(idx, _)| *idx);
296 let mut last_error: Option<camel_api::CamelError> = None;
297 if stop_on_exception {
298 let mut first_failed: Option<(usize, camel_api::CamelError)> = None;
299 for (idx, o) in &results {
300 if let camel_api::PipelineOutcome::Failed(err) = o
301 && first_failed
302 .as_ref()
303 .map(|(i, _)| *i > *idx)
304 .unwrap_or(true)
305 {
306 first_failed = Some((*idx, err.clone()));
307 }
308 }
309 if let Some((_, err)) = first_failed {
310 return camel_api::PipelineOutcome::Failed(err);
311 }
312 } else {
313 for (_, o) in &results {
315 if let camel_api::PipelineOutcome::Failed(err) = o {
316 last_error = Some(err.clone());
317 }
318 }
319 }
320
321 let failed_branches = results
324 .iter()
325 .filter(|(_, o)| matches!(o, camel_api::PipelineOutcome::Failed(_)))
326 .count();
327
328 let completed: Vec<Exchange> = results
330 .into_iter()
331 .filter_map(|(_, o)| match o {
332 camel_api::PipelineOutcome::Completed(ex) => Some(ex),
333 _ => None,
334 })
335 .collect();
336
337 if let Some(err) = last_error {
338 if completed.is_empty() {
339 return camel_api::PipelineOutcome::Failed(err);
340 }
341 tracing::warn!(
343 failed_branches,
344 branch_count = total,
345 "multicast partial success: discarding failed branch outcomes"
346 );
347 }
348 camel_api::PipelineOutcome::Completed((seg.aggregator)(completed))
349}
350
351#[cfg(test)]
352#[path = "multicast_segment_tests.rs"]
353mod tests;