Skip to main content

camel_core/lifecycle/adapters/
route_compiler.rs

1// adapters/route_compiler.rs
2// Pipeline compilation functions: compose BuilderSteps into a Tower BoxProcessor.
3// Tower types live here as this is the adapter layer responsible for
4// translating declarative route definitions into executable pipelines.
5
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::task::{Context, Poll};
10
11use tokio_util::sync::CancellationToken;
12use tower::Service;
13
14use camel_api::metrics::MetricsCollector;
15use camel_api::{
16    BoxProcessor, CamelError, Exchange, IdentityProcessor, Message, NoOpMetrics,
17    ORIGINAL_MESSAGE_EXTENSION, PipelineOutcome,
18};
19
20use camel_api::error_handler::{BoundaryKind, RetryOutcome, StepDisposition};
21use camel_processor::{
22    CircuitBreakerDecision, CircuitBreakerGate, RouteErrorHandler, invoke_processor,
23};
24use tracing::Instrument;
25
26use crate::lifecycle::adapters::body_coercing::wrap_if_needed;
27use crate::lifecycle::adapters::step_compilers::CompiledStep;
28use crate::shared::observability::adapters::TracingProcessor;
29use crate::shared::observability::domain::DetailLevel;
30
31// Re-export outcome composition types so existing step_compiler import paths
32// (`route_compiler::BoxProcessorSegment`, etc.) continue to work.
33pub(crate) use super::outcome_composition::{
34    BodyCoercingSegment, BoxProcessorSegment, StopSegment, compose_outcome_segment,
35};
36
37// Task-local cancel token — set by the pipeline task per-start, checked by
38// `run_steps` between steps. Absent in direct tests (skip check).
39//
40// Design: per-start task-local, NOT compiled into the pipeline struct, to
41// avoid the lifecycle bug where a compiled-in child token stays cancelled
42// after stop→restart (the new start would inherit the cancelled state).
43// ADR-0043.
44tokio::task_local! {
45    pub(crate) static CANCEL_TOKEN: CancellationToken;
46}
47
48/// Runtime context for metrics + route_id (B3). Cancel is via task-local (B1).
49#[derive(Clone)]
50pub struct PipelineRuntimeCtx {
51    pub metrics: Arc<dyn MetricsCollector>,
52    pub route_id: Arc<str>,
53}
54
55impl PipelineRuntimeCtx {
56    /// Constructor for compile-time contexts where no MetricsCollector is available.
57    /// The resulting pipeline will emit disposition counters to NoOpMetrics (no-op).
58    /// Prefer constructing PipelineRuntimeCtx with real metrics at route startup.
59    pub fn compile_time() -> Self {
60        Self {
61            metrics: Arc::new(NoOpMetrics),
62            route_id: Arc::from(""),
63        }
64    }
65}
66
67/// Newtype around `Arc<[CompiledStep]>`.
68///
69/// `CompiledStep` contains `BoxProcessor` (`tower::util::BoxCloneSyncService`),
70/// whose erased inner trait object is bounded `Send + Sync`. `CompiledStep` is
71/// therefore `Send + Sync` by construction, and `SharedSnapshot` derives both
72/// auto traits from `Arc<[CompiledStep]>` — the snapshot is shareable across
73/// threads with auto-derived traits alone.
74#[derive(Clone)]
75struct SharedSnapshot(Arc<[CompiledStep]>);
76
77// Compile-time guard: CompiledStep must remain Send + Sync so the snapshot
78// stays shareable via auto-derivation. `Send` keeps the future returned by
79// `run_steps` Send; `Sync` covers concurrent `&self` reads on
80// `SequentialPipeline`/`TracedPipeline` clones (e.g. `poll_ready` on one
81// thread, `call` on another).
82#[allow(dead_code)]
83const _: () = {
84    fn assert_send<T: Send>() {}
85    fn assert_sync<T: Sync>() {}
86    fn _check() {
87        assert_send::<CompiledStep>();
88        assert_sync::<CompiledStep>();
89    }
90};
91
92/// Compose a list of CompiledSteps into a sub-pipeline (EIP internal).
93///
94/// Uses `into_tower_result()` so `PipelineOutcome::Stopped` maps to `Ok(ex)`.
95/// Use [`compose_pipeline_with_handler`] for the top-level consumer-facing pipeline.
96pub fn compose_pipeline(processors: Vec<CompiledStep>, ctx: PipelineRuntimeCtx) -> BoxProcessor {
97    if processors.is_empty() {
98        return BoxProcessor::new(IdentityProcessor);
99    }
100    BoxProcessor::new(SequentialPipeline {
101        steps: SharedSnapshot(processors.into()),
102        handler: None,
103        ctx,
104    })
105}
106
107/// Compose a list of CompiledSteps with an optional route error handler.
108///
109/// When a handler is present, step readiness errors are swallowed (poll_ready
110/// returns Ready) and the handler's retry/recovery logic is invoked on step
111/// failures. Otherwise, step readiness errors propagate immediately.
112pub fn compose_pipeline_with_handler(
113    processors: Vec<CompiledStep>,
114    handler: Option<Arc<dyn RouteErrorHandler>>,
115    ctx: PipelineRuntimeCtx,
116) -> BoxProcessor {
117    if processors.is_empty() {
118        return BoxProcessor::new(IdentityProcessor);
119    }
120    BoxProcessor::new(SequentialPipeline {
121        steps: SharedSnapshot(processors.into()),
122        handler,
123        ctx,
124    })
125}
126
127/// Compose a list of CompiledSteps into a traced pipeline with Stop→Ok translation.
128///
129/// Each processor is wrapped with TracingProcessor to emit spans for observability.
130/// When tracing is disabled, falls back to [`compose_pipeline_with_handler`] with zero overhead.
131pub fn compose_traced_pipeline(
132    processors: Vec<CompiledStep>,
133    route_id: &str,
134    trace_enabled: bool,
135    detail_level: DetailLevel,
136    metrics: Option<Arc<dyn MetricsCollector>>,
137    handler: Option<Arc<dyn RouteErrorHandler>>,
138    ctx: PipelineRuntimeCtx,
139) -> BoxProcessor {
140    if !trace_enabled {
141        return compose_pipeline_with_handler(processors, handler, ctx);
142    }
143
144    if processors.is_empty() {
145        return BoxProcessor::new(IdentityProcessor);
146    }
147
148    let wrapped: Vec<CompiledStep> = processors
149        .into_iter()
150        .enumerate()
151        .map(|(idx, step)| {
152            let (p, c, lc) = match step {
153                CompiledStep::Process {
154                    processor,
155                    body_contract,
156                    lifecycle,
157                } => (processor, body_contract, lifecycle),
158                CompiledStep::Stop => return CompiledStep::Stop,
159                CompiledStep::Segment { .. } => return step,
160            };
161            let traced = BoxProcessor::new(TracingProcessor::new(
162                p,
163                route_id.to_string(),
164                idx,
165                detail_level.clone(),
166                metrics.clone(),
167            ));
168            CompiledStep::Process {
169                processor: traced,
170                body_contract: c,
171                lifecycle: lc,
172            }
173        })
174        .collect();
175
176    BoxProcessor::new(TracedPipeline {
177        steps: SharedSnapshot(wrapped.into()),
178        handler,
179        ctx,
180    })
181}
182
183/// Compose a list of `CompiledStep` items into a single pipeline with body coercion.
184///
185/// Each processor is optionally wrapped with `BodyCoercingProcessor` based on its
186/// contract. Processors with `None` contract are passed through with zero overhead.
187/// `CompiledStep::Stop` passes through without coercion.
188pub fn compose_pipeline_with_contracts(
189    processors: Vec<CompiledStep>,
190    handler: Option<Arc<dyn RouteErrorHandler>>,
191    ctx: PipelineRuntimeCtx,
192) -> BoxProcessor {
193    let wrapped: Vec<CompiledStep> = processors
194        .into_iter()
195        .map(|step| match step {
196            CompiledStep::Process {
197                processor,
198                body_contract,
199                lifecycle,
200            } => {
201                let coerced = wrap_if_needed(processor, body_contract);
202                CompiledStep::Process {
203                    processor: coerced,
204                    body_contract: None,
205                    lifecycle,
206                }
207            }
208            CompiledStep::Stop => CompiledStep::Stop,
209            CompiledStep::Segment { .. } => step,
210        })
211        .collect();
212    compose_pipeline_with_handler(wrapped, handler, ctx)
213}
214
215/// Compose a list of `CompiledStep` items into a traced pipeline with body coercion.
216///
217/// Applies body coercion contracts first, then wraps with `TracingProcessor`.
218/// When tracing is disabled, falls back to [`compose_pipeline_with_contracts`].
219pub(crate) fn compose_traced_pipeline_with_contracts(
220    processors: Vec<CompiledStep>,
221    route_id: &str,
222    trace_enabled: bool,
223    detail_level: DetailLevel,
224    metrics: Option<Arc<dyn MetricsCollector>>,
225    handler: Option<Arc<dyn RouteErrorHandler>>,
226    ctx: PipelineRuntimeCtx,
227) -> BoxProcessor {
228    if !trace_enabled {
229        return compose_pipeline_with_contracts(processors, handler, ctx);
230    }
231
232    if processors.is_empty() {
233        return BoxProcessor::new(IdentityProcessor);
234    }
235
236    let wrapped: Vec<CompiledStep> = processors
237        .into_iter()
238        .enumerate()
239        .map(|(idx, step)| match step {
240            CompiledStep::Process {
241                processor,
242                body_contract,
243                lifecycle,
244            } => {
245                let coerced = wrap_if_needed(processor, body_contract);
246                let traced = BoxProcessor::new(TracingProcessor::new(
247                    coerced,
248                    route_id.to_string(),
249                    idx,
250                    detail_level.clone(),
251                    metrics.clone(),
252                ));
253                CompiledStep::Process {
254                    processor: traced,
255                    body_contract: None,
256                    lifecycle,
257                }
258            }
259            CompiledStep::Stop => CompiledStep::Stop,
260            CompiledStep::Segment { .. } => step,
261        })
262        .collect();
263
264    BoxProcessor::new(TracedPipeline {
265        steps: SharedSnapshot(wrapped.into()),
266        handler,
267        ctx,
268    })
269}
270
271/// A service that executes a sequence of CompiledSteps in order.
272///
273/// Uses `into_tower_result()` so `PipelineOutcome::Stopped(ex)` maps to
274/// `Ok(ex)` — the Bug B fix that makes Stop indistinguishable from Completed
275/// at the consumer boundary.
276#[derive(Clone)]
277struct SequentialPipeline {
278    steps: SharedSnapshot,
279    handler: Option<Arc<dyn RouteErrorHandler>>,
280    ctx: PipelineRuntimeCtx,
281}
282
283impl Service<Exchange> for SequentialPipeline {
284    type Response = Exchange;
285    type Error = CamelError;
286    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
287
288    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
289        match self.steps.0.first() {
290            Some(CompiledStep::Process { processor, .. }) => {
291                let mut proc = processor.clone();
292                match proc.poll_ready(cx) {
293                    Poll::Pending => Poll::Pending,
294                    Poll::Ready(Err(_)) if self.handler.is_some() => Poll::Ready(Ok(())),
295                    Poll::Ready(other) => Poll::Ready(other),
296                }
297            }
298            Some(CompiledStep::Stop) => Poll::Ready(Ok(())),
299            Some(CompiledStep::Segment { .. }) => Poll::Ready(Ok(())),
300            None => Poll::Ready(Ok(())),
301        }
302    }
303
304    // ADR-0024 reply-channel adapter: PipelineOutcome → Result<Exchange, CamelError>.
305    // Completed(ex) and Stopped(ex) both map to Ok(ex); Failed(err) maps to Err.
306    // Downstream consumers (RouteChannelService, ExchangeUoWLayer, HTTP/Kafka reply
307    // finalisers) see Result<Exchange, CamelError> and treat Stop as success.
308    fn call(&mut self, exchange: Exchange) -> Self::Future {
309        // Cheap Arc::clone (refcount bump) on the SharedSnapshot newtype.
310        // `SharedSnapshot: Send` so the future returned by `run_steps`
311        // captures it directly without needing a Send-asserting wrapper.
312        let steps = self.steps.clone();
313        let handler = self.handler.clone();
314        let ctx = self.ctx.clone();
315        Box::pin(async move {
316            run_steps(steps, exchange, handler, false, &ctx)
317                .await
318                .into_tower_result()
319        })
320    }
321}
322
323/// A traced service pipeline for wrapped CompiledSteps.
324#[derive(Clone)]
325struct TracedPipeline {
326    steps: SharedSnapshot,
327    handler: Option<Arc<dyn RouteErrorHandler>>,
328    ctx: PipelineRuntimeCtx,
329}
330
331impl Service<Exchange> for TracedPipeline {
332    type Response = Exchange;
333    type Error = CamelError;
334    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
335
336    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
337        match self.steps.0.first() {
338            Some(CompiledStep::Process { processor, .. }) => {
339                let mut proc = processor.clone();
340                match proc.poll_ready(cx) {
341                    Poll::Pending => Poll::Pending,
342                    Poll::Ready(Err(_)) if self.handler.is_some() => Poll::Ready(Ok(())),
343                    Poll::Ready(other) => Poll::Ready(other),
344                }
345            }
346            Some(CompiledStep::Stop) => Poll::Ready(Ok(())),
347            Some(CompiledStep::Segment { .. }) => Poll::Ready(Ok(())),
348            None => Poll::Ready(Ok(())),
349        }
350    }
351
352    // ADR-0024 reply-channel adapter (same as SequentialPipeline::call):
353    // Completed(ex) and Stopped(ex) both map to Ok(ex). Bug B fix.
354    fn call(&mut self, exchange: Exchange) -> Self::Future {
355        let steps = self.steps.clone();
356        let handler = self.handler.clone();
357        let ctx = self.ctx.clone();
358        Box::pin(async move {
359            run_steps(steps, exchange, handler, true, &ctx)
360                .await
361                .into_tower_result()
362        })
363    }
364}
365
366/// Run a sequence of CompiledSteps with optional error recovery.
367///
368/// Each step is unified under [`OwnedRetryable`] — both Process and
369/// Segment variants are treated uniformly via a stack-allocated enum
370/// that dispatches to the existing `RetryableStep` impls on
371/// `BoxProcessor` and `OutcomeSegment`. This eliminates the per-step
372/// `Box::new(...) as Box<dyn RetryableStep>` heap allocation that the
373/// pre-A2 implementation paid for every step of every Exchange (A2).
374///
375/// On failure:
376/// 1. If a handler is present, `match_policy` selects a retry policy.
377/// 2. `retry_step` attempts recovery; if exhausted, `handle_step` determines
378///    the disposition:
379///    - `Propagate` — return the error
380///    - `Handled` — return the exchange early (success)
381///    - `Continued` — clear the error and continue to the next step
382/// 3. If no handler is present, the error is propagated directly.
383///
384/// CompiledStep::Stop short-circuits to `PipelineOutcome::Stopped(ex)` — the
385/// handler is bypassed and no Tower service is invoked (ADR-0024 §3.5).
386async fn run_steps(
387    steps: SharedSnapshot,
388    exchange: Exchange,
389    handler: Option<Arc<dyn RouteErrorHandler>>,
390    trace: bool,
391    ctx: &PipelineRuntimeCtx,
392) -> PipelineOutcome {
393    use camel_api::error_handler::RetryableStep;
394    let mut ex = exchange;
395    // Index-based loop (not `for (i, step) in steps.0.iter().enumerate()`):
396    // retained to avoid holding a `&[CompiledStep]` borrow across the
397    // `.await` below — `&steps.0[i]` is consumed by the `match` scrutinee
398    // and drops before the await, so no borrow is live across the await
399    // point. The original `CompiledStep: !Sync` rationale is gone
400    // (`BoxProcessor` is now `Send + Sync` via `BoxCloneSyncService`);
401    // the loop shape is kept purely for borrow hygiene — no behavior change.
402    let len = steps.0.len();
403    for i in 0..len {
404        // B1: cooperative cancellation between steps via task-local.
405        // If the task-local is not set (direct test calls), skip the check.
406        let cancelled = CANCEL_TOKEN.try_with(|t| t.is_cancelled()).unwrap_or(false);
407        if cancelled {
408            return PipelineOutcome::Failed(CamelError::ConsumerStopping);
409        }
410        // A2: dispatch to existing `RetryableStep` impls through a stack
411        // enum instead of paying `Box::new(...) as Box<dyn RetryableStep>`
412        // per step. `OwnedRetryable` is `enum { Processor, Segment }` with
413        // discriminant-by-value layout — no extra heap alloc.
414        let mut retryable: OwnedRetryable = match &steps.0[i] {
415            CompiledStep::Stop => return PipelineOutcome::Stopped(ex),
416            CompiledStep::Process { processor, .. } => OwnedRetryable::Processor(processor.clone()),
417            CompiledStep::Segment { segment, .. } => OwnedRetryable::Segment(segment.clone()),
418        };
419
420        let original = handler.as_ref().map(|_| ex.clone());
421        let outcome = if trace {
422            invoke_with_span(&mut retryable, ex, i).await
423        } else {
424            retryable.invoke(ex).await
425        };
426
427        match outcome {
428            PipelineOutcome::Completed(next) => {
429                if camel_api::is_camel_stop(&next) {
430                    return PipelineOutcome::Stopped(next);
431                }
432                ex = next;
433            }
434            PipelineOutcome::Stopped(stopped_ex) => {
435                return PipelineOutcome::Stopped(stopped_ex);
436            }
437            PipelineOutcome::Failed(err) => {
438                let (Some(handler), Some(original)) = (handler.as_ref(), original) else {
439                    return PipelineOutcome::Failed(err);
440                };
441                let policy = handler.match_policy(&err);
442                // `&mut retryable` auto-coerces from `&mut OwnedRetryable` to
443                // `&mut dyn RetryableStep` via the trait impl on the enum.
444                match handler
445                    .retry_step(policy, &mut retryable, original, err)
446                    .await
447                {
448                    RetryOutcome::Recovered(exchange) => {
449                        ctx.metrics.record_counter(
450                            "pipeline_disposition",
451                            1.0,
452                            &[("disposition", "recovered"), ("route_id", &ctx.route_id)],
453                        );
454                        ex = exchange;
455                    }
456                    RetryOutcome::Stopped(stopped_ex) => {
457                        ctx.metrics.record_counter(
458                            "pipeline_disposition",
459                            1.0,
460                            &[("disposition", "stopped"), ("route_id", &ctx.route_id)],
461                        );
462                        return PipelineOutcome::Stopped(stopped_ex);
463                    }
464                    RetryOutcome::Exhausted {
465                        exchange,
466                        error,
467                        policy,
468                    } => {
469                        let disposition = if trace {
470                            handler
471                                .handle_step(policy, exchange, error)
472                                .instrument(tracing::debug_span!("error_handler", step_index = i))
473                                .await
474                        } else {
475                            handler.handle_step(policy, exchange, error).await
476                        };
477                        match disposition {
478                            Ok(StepDisposition::Propagate(e)) => {
479                                ctx.metrics.record_counter(
480                                    "pipeline_disposition",
481                                    1.0,
482                                    &[("disposition", "propagated"), ("route_id", &ctx.route_id)],
483                                );
484                                return PipelineOutcome::Failed(e);
485                            }
486                            Ok(StepDisposition::Handled(done)) => {
487                                ctx.metrics.record_counter(
488                                    "pipeline_disposition",
489                                    1.0,
490                                    &[("disposition", "handled"), ("route_id", &ctx.route_id)],
491                                );
492                                return PipelineOutcome::Completed(done);
493                            }
494                            Ok(StepDisposition::Continued(next)) => {
495                                ctx.metrics.record_counter(
496                                    "pipeline_disposition",
497                                    1.0,
498                                    &[("disposition", "continued"), ("route_id", &ctx.route_id)],
499                                );
500                                ex = next;
501                            }
502                            Err(e) => {
503                                ctx.metrics.record_counter(
504                                    "pipeline_disposition",
505                                    1.0,
506                                    &[
507                                        ("disposition", "handler_error"),
508                                        ("route_id", &ctx.route_id),
509                                    ],
510                                );
511                                return PipelineOutcome::Failed(e);
512                            }
513                            // Future StepDisposition variants fail the pipeline.
514                            _ => {
515                                return PipelineOutcome::Failed(CamelError::ProcessorError(
516                                    "unknown step disposition".to_string(),
517                                ));
518                            }
519                        }
520                    }
521                    // Future RetryOutcome variants fail the pipeline.
522                    _ => {
523                        return PipelineOutcome::Failed(CamelError::ProcessorError(
524                            "unknown retry outcome".to_string(),
525                        ));
526                    }
527                }
528            }
529        }
530    }
531    PipelineOutcome::Completed(ex)
532}
533
534/// Stack-allocated dispatcher that unifies `BoxProcessor` and
535/// `OutcomeSegment` for the retry path without the heap allocation a
536/// `Box<dyn RetryableStep>` would require. Sized by-value, dispatched
537/// through a single trait method that fans out to the existing
538/// `RetryableStep` impls on each variant.
539///
540/// A2: replaces `Box::new(processor.clone()) as Box<dyn RetryableStep>`
541/// (and the equivalent for segments) with this enum, saving one heap
542/// allocation per pipeline step per Exchange invocation.
543enum OwnedRetryable {
544    Processor(camel_api::BoxProcessor),
545    Segment(camel_api::OutcomeSegment),
546}
547
548impl camel_api::error_handler::RetryableStep for OwnedRetryable {
549    fn invoke<'a>(
550        &'a mut self,
551        exchange: Exchange,
552    ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
553        match self {
554            OwnedRetryable::Processor(p) => p.invoke(exchange),
555            OwnedRetryable::Segment(s) => s.invoke(exchange),
556        }
557    }
558}
559
560async fn invoke_with_span(
561    retryable: &mut dyn camel_api::error_handler::RetryableStep,
562    exchange: Exchange,
563    idx: usize,
564) -> PipelineOutcome {
565    retryable
566        .invoke(exchange)
567        .instrument(tracing::debug_span!("pipeline_step", index = idx))
568        .await
569}
570
571/// Route channel with explicit security and circuit-breaker gates.
572///
573/// Gate order: Security → CB(before_call) → Pipeline → CB(after_result).
574/// Errors from Security/CB gates go to `handler.handle_boundary`.
575/// Errors from Pipeline go through the injected handler's retry/handle_step.
576/// Pipeline Propagate returns Err — passed through to upstream.
577#[derive(Clone)]
578pub struct RouteChannelService {
579    handler: Arc<dyn RouteErrorHandler>,
580    security: Option<BoxProcessor>,
581    cb_gate: Option<CircuitBreakerGate>,
582    pipeline: BoxProcessor,
583    /// When true, stash the original Message as `ORIGINAL_MESSAGE_EXTENSION`
584    /// before any gate runs, so the error handler can restore it on failure.
585    use_original_message: bool,
586}
587
588impl RouteChannelService {
589    pub fn new(
590        handler: Arc<dyn RouteErrorHandler>,
591        security: Option<BoxProcessor>,
592        cb_gate: Option<CircuitBreakerGate>,
593        pipeline: BoxProcessor,
594        use_original_message: bool,
595    ) -> Self {
596        Self {
597            handler,
598            security,
599            cb_gate,
600            pipeline,
601            use_original_message,
602        }
603    }
604}
605
606impl Service<Exchange> for RouteChannelService {
607    type Response = Exchange;
608    type Error = CamelError;
609    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
610
611    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), CamelError>> {
612        // Swallow readiness errors from security gate — deferred to call()
613        if let Some(ref mut sec) = self.security {
614            match sec.clone().poll_ready(cx) {
615                Poll::Pending => return Poll::Pending,
616                Poll::Ready(Err(_)) | Poll::Ready(Ok(())) => {}
617            }
618        }
619        // Pipeline readiness — swallow errors when handler present
620        match self.pipeline.clone().poll_ready(cx) {
621            Poll::Pending => return Poll::Pending,
622            Poll::Ready(Err(_)) | Poll::Ready(Ok(())) => {}
623        }
624        Poll::Ready(Ok(()))
625    }
626
627    fn call(&mut self, exchange: Exchange) -> Self::Future {
628        let handler = self.handler.clone();
629        let security = self.security.clone();
630        let cb_gate = self.cb_gate.clone();
631        let mut pipeline = self.pipeline.clone();
632        let use_original_message = self.use_original_message;
633
634        Box::pin(async move {
635            let mut ex = exchange;
636
637            // Stash original message for use_original_message support.
638            // Done BEFORE any gate so the DLC can restore the pre-route message.
639            // Only stashes when the flag is true to avoid perf regression on every Exchange.
640            if use_original_message {
641                let original: Arc<Message> = Arc::new(ex.input.clone());
642                ex.set_extension(ORIGINAL_MESSAGE_EXTENSION, original);
643            }
644
645            // Gate 1: Security
646            if let Some(mut sec) = security {
647                let original = ex.clone();
648                match invoke_processor(&mut sec, ex).await {
649                    Ok(next) => ex = next,
650                    Err(err) => {
651                        return handler
652                            .handle_boundary(BoundaryKind::Security, original, err)
653                            .await;
654                    }
655                }
656            }
657
658            // Gate 2: CircuitBreaker — before_call
659            if let Some(ref cb) = cb_gate {
660                match cb.before_call() {
661                    CircuitBreakerDecision::Allow => { /* proceed to pipeline */ }
662                    CircuitBreakerDecision::Fallback(mut fb) => {
663                        // Circuit open with fallback — call fallback.
664                        // Fallback errors go through handle_boundary, not raw to upstream.
665                        let original = ex.clone();
666                        match invoke_processor(&mut fb, ex).await {
667                            Ok(result) => return Ok(result),
668                            Err(err) => {
669                                return handler
670                                    .handle_boundary(BoundaryKind::CircuitBreaker, original, err)
671                                    .await;
672                            }
673                        }
674                    }
675                    CircuitBreakerDecision::Reject(err) => {
676                        let original = ex.clone();
677                        return handler
678                            .handle_boundary(BoundaryKind::CircuitBreaker, original, err)
679                            .await;
680                    }
681                }
682            }
683
684            // Pipeline (handler already injected for step errors)
685            let result = invoke_processor(&mut pipeline, ex).await;
686
687            // Gate 2: CircuitBreaker — after_result
688            if let Some(ref cb) = cb_gate {
689                cb.after_result(&result);
690            }
691
692            // Propagate from inner handler — pass through to upstream
693            result
694        })
695    }
696}
697
698#[cfg(test)]
699#[path = "route_compiler_tests.rs"]
700mod tests;