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 // Future StepDisposition variants fail the pipeline.
531 _ => {
532 return PipelineOutcome::Failed(CamelError::ProcessorError(
533 "unknown step disposition".to_string(),
534 ));
535 }
536 }
537 }
538 // Future RetryOutcome variants fail the pipeline.
539 _ => {
540 return PipelineOutcome::Failed(CamelError::ProcessorError(
541 "unknown retry outcome".to_string(),
542 ));
543 }
544 }
545 }
546 }
547 }
548 PipelineOutcome::Completed(ex)
549}
550
551/// Stack-allocated dispatcher that unifies `BoxProcessor` and
552/// `OutcomeSegment` for the retry path without the heap allocation a
553/// `Box<dyn RetryableStep>` would require. Sized by-value, dispatched
554/// through a single trait method that fans out to the existing
555/// `RetryableStep` impls on each variant.
556///
557/// A2: replaces `Box::new(processor.clone()) as Box<dyn RetryableStep>`
558/// (and the equivalent for segments) with this enum, saving one heap
559/// allocation per pipeline step per Exchange invocation.
560enum OwnedRetryable {
561 Processor(camel_api::BoxProcessor),
562 Segment(camel_api::OutcomeSegment),
563}
564
565impl camel_api::error_handler::RetryableStep for OwnedRetryable {
566 fn invoke<'a>(
567 &'a mut self,
568 exchange: Exchange,
569 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
570 match self {
571 OwnedRetryable::Processor(p) => p.invoke(exchange),
572 OwnedRetryable::Segment(s) => s.invoke(exchange),
573 }
574 }
575}
576
577async fn invoke_with_span(
578 retryable: &mut dyn camel_api::error_handler::RetryableStep,
579 exchange: Exchange,
580 idx: usize,
581) -> PipelineOutcome {
582 retryable
583 .invoke(exchange)
584 .instrument(tracing::debug_span!("pipeline_step", index = idx))
585 .await
586}
587
588/// Route channel with explicit security and circuit-breaker gates.
589///
590/// Gate order: Security → CB(before_call) → Pipeline → CB(after_result).
591/// Errors from Security/CB gates go to `handler.handle_boundary`.
592/// Errors from Pipeline go through the injected handler's retry/handle_step.
593/// Pipeline Propagate returns Err — passed through to upstream.
594#[derive(Clone)]
595pub struct RouteChannelService {
596 handler: Arc<dyn RouteErrorHandler>,
597 security: Option<BoxProcessor>,
598 cb_gate: Option<CircuitBreakerGate>,
599 pipeline: BoxProcessor,
600 /// When true, stash the original Message as `ORIGINAL_MESSAGE_EXTENSION`
601 /// before any gate runs, so the error handler can restore it on failure.
602 use_original_message: bool,
603}
604
605impl RouteChannelService {
606 pub fn new(
607 handler: Arc<dyn RouteErrorHandler>,
608 security: Option<BoxProcessor>,
609 cb_gate: Option<CircuitBreakerGate>,
610 pipeline: BoxProcessor,
611 use_original_message: bool,
612 ) -> Self {
613 Self {
614 handler,
615 security,
616 cb_gate,
617 pipeline,
618 use_original_message,
619 }
620 }
621}
622
623impl Service<Exchange> for RouteChannelService {
624 type Response = Exchange;
625 type Error = CamelError;
626 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
627
628 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), CamelError>> {
629 // Swallow readiness errors from security gate — deferred to call()
630 if let Some(ref mut sec) = self.security {
631 match sec.clone().poll_ready(cx) {
632 Poll::Pending => return Poll::Pending,
633 Poll::Ready(Err(_)) | Poll::Ready(Ok(())) => {}
634 }
635 }
636 // Pipeline readiness — swallow errors when handler present
637 match self.pipeline.clone().poll_ready(cx) {
638 Poll::Pending => return Poll::Pending,
639 Poll::Ready(Err(_)) | Poll::Ready(Ok(())) => {}
640 }
641 Poll::Ready(Ok(()))
642 }
643
644 fn call(&mut self, exchange: Exchange) -> Self::Future {
645 let handler = self.handler.clone();
646 let security = self.security.clone();
647 let cb_gate = self.cb_gate.clone();
648 let mut pipeline = self.pipeline.clone();
649 let use_original_message = self.use_original_message;
650
651 Box::pin(async move {
652 let mut ex = exchange;
653
654 // Stash original message for use_original_message support.
655 // Done BEFORE any gate so the DLC can restore the pre-route message.
656 // Only stashes when the flag is true to avoid perf regression on every Exchange.
657 if use_original_message {
658 let original: Arc<Message> = Arc::new(ex.input.clone());
659 ex.set_extension(ORIGINAL_MESSAGE_EXTENSION, original);
660 }
661
662 // Gate 1: Security
663 if let Some(mut sec) = security {
664 let original = ex.clone();
665 match invoke_processor(&mut sec, ex).await {
666 Ok(next) => ex = next,
667 Err(err) => {
668 return handler
669 .handle_boundary(BoundaryKind::Security, original, err)
670 .await;
671 }
672 }
673 }
674
675 // Gate 2: CircuitBreaker — before_call
676 if let Some(ref cb) = cb_gate {
677 match cb.before_call() {
678 CircuitBreakerDecision::Allow => { /* proceed to pipeline */ }
679 CircuitBreakerDecision::Fallback(mut fb) => {
680 // Circuit open with fallback — call fallback.
681 // Fallback errors go through handle_boundary, not raw to upstream.
682 let original = ex.clone();
683 match invoke_processor(&mut fb, ex).await {
684 Ok(result) => return Ok(result),
685 Err(err) => {
686 return handler
687 .handle_boundary(BoundaryKind::CircuitBreaker, original, err)
688 .await;
689 }
690 }
691 }
692 CircuitBreakerDecision::Reject(err) => {
693 let original = ex.clone();
694 return handler
695 .handle_boundary(BoundaryKind::CircuitBreaker, original, err)
696 .await;
697 }
698 }
699 }
700
701 // Pipeline (handler already injected for step errors)
702 let result = invoke_processor(&mut pipeline, ex).await;
703
704 // Gate 2: CircuitBreaker — after_result
705 if let Some(ref cb) = cb_gate {
706 cb.after_result(&result);
707 }
708
709 // Propagate from inner handler — pass through to upstream
710 result
711 })
712 }
713}
714
715#[cfg(test)]
716#[path = "route_compiler_tests.rs"]
717mod tests;