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
17use crate::multicast::{CAMEL_MULTICAST_COMPLETE, CAMEL_MULTICAST_INDEX};
18
19#[derive(Clone)]
33pub struct MulticastSegment {
34 pub branches: Vec<camel_api::OutcomeSegment>,
35 pub parallel: bool,
36 pub parallel_limit: Option<usize>,
38 pub stop_on_exception: bool,
49 pub timeout: Option<Duration>,
51 pub aggregator: Arc<dyn Fn(Vec<Exchange>) -> Exchange + Send + Sync>,
52}
53
54impl camel_api::OutcomePipeline for MulticastSegment {
55 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
56 Box::new(self.clone())
57 }
58
59 fn run<'a>(
60 &'a mut self,
61 exchange: Exchange,
62 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
63 Box::pin(async move {
64 if self.parallel {
65 parallel_multicast(self, exchange).await
66 } else {
67 sequential_multicast(self, exchange).await
68 }
69 })
70 }
71}
72
73async fn sequential_multicast(
76 seg: &mut MulticastSegment,
77 exchange: Exchange,
78) -> camel_api::PipelineOutcome {
79 let mut outputs = Vec::new();
80 let mut last_error: Option<camel_api::CamelError> = None;
81 let total = seg.branches.len();
82 for (i, branch) in seg.branches.iter_mut().enumerate() {
83 let mut ex = exchange.clone();
85 ex.set_property(CAMEL_MULTICAST_INDEX, Value::from(i as i64));
86 ex.set_property(CAMEL_MULTICAST_COMPLETE, Value::Bool(i == total - 1));
87 match branch.run(ex).await {
88 camel_api::PipelineOutcome::Completed(ex) => outputs.push(ex),
89 camel_api::PipelineOutcome::Stopped(ex) => {
90 return camel_api::PipelineOutcome::Stopped(ex);
91 }
92 camel_api::PipelineOutcome::Failed(err) => {
93 if seg.stop_on_exception {
94 return camel_api::PipelineOutcome::Failed(err);
95 }
96 last_error = Some(err);
98 }
99 }
100 }
101 if let Some(err) = last_error {
102 if outputs.is_empty() {
103 return camel_api::PipelineOutcome::Failed(err);
104 }
105 tracing::warn!(
107 failed_branches = total - outputs.len(),
108 branch_count = total,
109 "multicast partial success: discarding failed branch outcomes"
110 );
111 }
112 camel_api::PipelineOutcome::Completed((seg.aggregator)(outputs))
113}
114
115async fn parallel_multicast(
122 seg: &mut MulticastSegment,
123 exchange: Exchange,
124) -> camel_api::PipelineOutcome {
125 use std::sync::Arc;
126 use tokio::sync::Semaphore;
127
128 let stopped_seen = Arc::new(AtomicBool::new(false));
129 let stopped_idx = Arc::new(AtomicUsize::new(usize::MAX));
130 let semaphore = seg
131 .parallel_limit
132 .filter(|&limit| limit > 0)
133 .map(|limit| Arc::new(Semaphore::new(limit)));
134 let timeout = seg.timeout;
135 let stop_on_exception = seg.stop_on_exception;
136 let total = seg.branches.len();
137
138 let mut set: JoinSet<(usize, Option<camel_api::PipelineOutcome>)> = JoinSet::new();
139
140 for (idx, mut branch) in seg.branches.clone().into_iter().enumerate() {
141 let stopped_seen = Arc::clone(&stopped_seen);
142 let stopped_idx = Arc::clone(&stopped_idx);
143 let sem = semaphore.clone();
144 let mut ex = exchange.clone();
146 ex.set_property(CAMEL_MULTICAST_INDEX, Value::from(idx as i64));
147 ex.set_property(CAMEL_MULTICAST_COMPLETE, Value::Bool(idx == total - 1));
148 set.spawn(async move {
149 if stopped_seen.load(Ordering::SeqCst) {
151 return (idx, None);
152 }
153 let _permit: Option<tokio::sync::OwnedSemaphorePermit> = match &sem {
155 Some(s) => match Arc::clone(s).acquire_owned().await {
156 Ok(p) => Some(p),
157 Err(_) => {
158 return (
159 idx,
160 Some(camel_api::PipelineOutcome::Failed(
161 camel_api::CamelError::ProcessorError("semaphore closed".into()),
162 )),
163 );
164 }
165 },
166 None => None,
167 };
168 if stopped_seen.load(Ordering::SeqCst) {
170 return (idx, None);
171 }
172
173 let outcome = async {
176 let outcome = branch.run(ex).await;
177 if let camel_api::PipelineOutcome::Stopped(_) = &outcome {
178 loop {
180 let cur = stopped_idx.load(Ordering::SeqCst);
181 if idx >= cur {
182 break;
183 }
184 match stopped_idx.compare_exchange_weak(
185 cur,
186 idx,
187 Ordering::SeqCst,
188 Ordering::SeqCst,
189 ) {
190 Ok(_) => break,
191 Err(actual) => {
192 if actual <= idx {
193 break;
194 }
195 }
196 }
197 }
198 stopped_seen.store(true, Ordering::SeqCst);
199 }
200 outcome
201 };
202
203 let outcome = AssertUnwindSafe(outcome).catch_unwind();
213 let outcome = if let Some(dur) = timeout {
214 match tokio::time::timeout(dur, outcome).await {
215 Ok(Ok(o)) => o,
216 Ok(Err(panic_payload)) => {
217 let failure = camel_api::PipelineOutcome::Failed(
218 camel_api::CamelError::ProcessorError(format!(
219 "multicast branch {idx} panicked"
220 )),
221 );
222 std::mem::forget(panic_payload);
223 failure
224 }
225 Err(_elapsed) => {
226 camel_api::PipelineOutcome::Failed(camel_api::CamelError::ProcessorError(
227 format!("multicast branch {idx} timed out after {dur:?}"),
228 ))
229 }
230 }
231 } else {
232 match outcome.await {
233 Ok(outcome) => outcome,
234 Err(panic_payload) => {
235 let failure = camel_api::PipelineOutcome::Failed(
236 camel_api::CamelError::ProcessorError(format!(
237 "multicast branch {idx} panicked"
238 )),
239 );
240 std::mem::forget(panic_payload);
241 failure
242 }
243 }
244 };
245
246 (idx, Some(outcome))
247 });
248 }
249
250 let mut results: Vec<(usize, camel_api::PipelineOutcome)> = Vec::new();
252 while let Some(res) = set.join_next().await {
253 if let Ok((idx, Some(o))) = res {
254 results.push((idx, o));
255 }
256 }
257
258 if stopped_seen.load(Ordering::SeqCst) {
260 let winning_idx = stopped_idx.load(Ordering::SeqCst);
261 if winning_idx == usize::MAX {
262 tracing::warn!(
263 target: "camel.phase4.multicast",
264 "stopped_seen=true but stopped_idx=usize::MAX — race; falling back to pre-multicast exchange"
265 );
266 return camel_api::PipelineOutcome::Stopped(exchange);
267 }
268 let stopped_ex = results
269 .iter()
270 .find(|(idx, _)| *idx == winning_idx)
271 .and_then(|(_, o)| match o {
272 camel_api::PipelineOutcome::Stopped(ex) => Some(ex.clone()),
273 _ => None,
274 });
275 if let Some(ex) = stopped_ex {
276 return camel_api::PipelineOutcome::Stopped(ex);
277 }
278 tracing::warn!(
279 target: "camel.phase4.multicast",
280 winning_idx = winning_idx,
281 "winning_idx not found — falling back to pre-multicast exchange"
282 );
283 return camel_api::PipelineOutcome::Stopped(exchange);
284 }
285
286 results.sort_by_key(|(idx, _)| *idx);
291 let mut last_error: Option<camel_api::CamelError> = None;
292 if stop_on_exception {
293 let mut first_failed: Option<(usize, camel_api::CamelError)> = None;
294 for (idx, o) in &results {
295 if let camel_api::PipelineOutcome::Failed(err) = o
296 && first_failed
297 .as_ref()
298 .map(|(i, _)| *i > *idx)
299 .unwrap_or(true)
300 {
301 first_failed = Some((*idx, err.clone()));
302 }
303 }
304 if let Some((_, err)) = first_failed {
305 return camel_api::PipelineOutcome::Failed(err);
306 }
307 } else {
308 for (_, o) in &results {
310 if let camel_api::PipelineOutcome::Failed(err) = o {
311 last_error = Some(err.clone());
312 }
313 }
314 }
315
316 let failed_branches = results
319 .iter()
320 .filter(|(_, o)| matches!(o, camel_api::PipelineOutcome::Failed(_)))
321 .count();
322
323 let completed: Vec<Exchange> = results
325 .into_iter()
326 .filter_map(|(_, o)| match o {
327 camel_api::PipelineOutcome::Completed(ex) => Some(ex),
328 _ => None,
329 })
330 .collect();
331
332 if let Some(err) = last_error {
333 if completed.is_empty() {
334 return camel_api::PipelineOutcome::Failed(err);
335 }
336 tracing::warn!(
338 failed_branches,
339 branch_count = total,
340 "multicast partial success: discarding failed branch outcomes"
341 );
342 }
343 camel_api::PipelineOutcome::Completed((seg.aggregator)(completed))
344}
345
346#[cfg(test)]
347#[path = "multicast_segment_tests.rs"]
348mod tests;