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