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;