1use std::sync::Arc;
7use std::time::Duration;
8
9use tokio::sync::mpsc;
10use tokio_util::sync::CancellationToken;
11use tower::Service;
12use tracing::{error, info, warn};
13
14use camel_api::security_policy::RouteSecurityPlan;
15use camel_api::{CamelError, NoOpMetrics, StepLifecycle, StepShutdownReason};
16use camel_component_api::Consumer;
17use camel_component_api::{ConcurrencyModel, ConsumerContext, consumer::ExchangeEnvelope};
18
19use crate::lifecycle::adapters::consumer_management;
20use crate::lifecycle::adapters::controller_component_context::ControllerComponentContext;
21use crate::lifecycle::adapters::route_compiler::CANCEL_TOKEN;
22use crate::lifecycle::adapters::route_controller::DefaultRouteController;
23#[cfg(test)]
24use crate::lifecycle::adapters::route_helpers::emit_start_route_event;
25use crate::lifecycle::adapters::route_helpers::{
26 DrainGuard, handle_is_running, inferred_lifecycle_label, ready_with_backoff,
27};
28use crate::lifecycle::adapters::route_registry::DEFAULT_SHUTDOWN_TIMEOUT;
29use crate::lifecycle::adapters::route_runtime_state::CompiledRoute;
30
31pub use camel_auth::bind_gate::{BindExposureAcks, enforce_bind_exposure_gate};
40
41pub(super) struct BindKey {
46 pub(super) key: String,
47 pub(super) loopback: bool,
48}
49
50pub(super) fn bind_key_from_uri(uri: &str) -> Option<BindKey> {
51 let scheme = uri.split(':').next()?;
52 if !matches!(scheme, "http" | "https" | "ws" | "wss" | "grpc") {
53 return None;
54 }
55 let authority = uri.split("://").nth(1)?;
56 let authority = authority.split('/').next()?;
57 if authority.is_empty() {
58 return None;
59 }
60 if let Ok(addr) = authority.parse::<std::net::SocketAddr>() {
61 return Some(BindKey {
62 key: addr.to_string(),
63 loopback: addr.ip().is_loopback(),
64 });
65 }
66 let host = authority
71 .rsplit_once(':')
72 .map(|(h, _)| h)
73 .unwrap_or(authority)
74 .trim_matches(['[', ']']);
75 let loopback = host.eq_ignore_ascii_case("localhost");
76 Some(BindKey {
77 key: authority.to_string(),
78 loopback,
79 })
80}
81
82fn deliver_security_context(consumer: &mut dyn Consumer, compiled: &CompiledRoute) {
92 use camel_component_api::SecurityContext;
93
94 if let (Some(sp_config), Some(_)) = (
95 compiled.security_policy.as_ref(),
96 compiled.security_authenticator.as_ref(),
97 ) {
98 let mut sec_ctx = SecurityContext::from_arc(Arc::clone(&sp_config.policy))
99 .with_credential_sources(sp_config.credential_sources.clone());
100 if let Some(registry) = &compiled.provider_registry {
104 sec_ctx = sec_ctx.with_providers(Arc::clone(registry));
105 }
106 if let Some(plan) = &compiled.security_plan {
109 sec_ctx = sec_ctx.with_plan(plan.clone());
110 }
111 consumer.set_security_context(sec_ctx);
112 } else if let Some(plan) = compiled.security_plan.clone() {
113 let mut sec_ctx = SecurityContext::from_plan(plan);
118 if let Some(registry) = &compiled.provider_registry {
119 sec_ctx = sec_ctx.with_providers(Arc::clone(registry));
120 }
121 consumer.set_security_context(sec_ctx);
122 }
123}
124
125fn strict_dispatch_denies(
133 dispatch_plan: &Option<camel_api::security_policy::RouteSecurityPlan>,
134 exchange: &camel_api::Exchange,
135 reply_tx: &mut Option<tokio::sync::oneshot::Sender<Result<camel_api::Exchange, CamelError>>>,
136 route_id: &str,
137) -> bool {
138 if let Some(plan) = dispatch_plan.as_ref()
139 && let Err(denial) = camel_auth::enforce_dispatch(plan, exchange)
140 {
141 if let Some(tx) = reply_tx.take() {
142 let _ = tx.send(Err(denial));
143 } else {
144 warn!(
146 route_id = %route_id,
147 error = %denial,
148 "dispatch denied: no kernel carrier on Exchange"
149 );
150 }
151 return true;
152 }
153 false
154}
155
156#[cfg(test)]
157mod bind_key_tests {
158 use super::bind_key_from_uri;
159
160 #[test]
161 fn https_and_wss_listeners_gate() {
162 assert_eq!(
163 bind_key_from_uri("https://0.0.0.0:8443/api").map(|b| b.key),
164 Some("0.0.0.0:8443".to_string())
165 );
166 assert_eq!(
167 bind_key_from_uri("wss://0.0.0.0:9000").map(|b| b.key),
168 Some("0.0.0.0:9000".to_string())
169 );
170 }
171
172 #[test]
173 fn non_listener_schemes_skip() {
174 assert!(bind_key_from_uri("timer:tick?period=1s").is_none());
175 assert!(bind_key_from_uri("mcp:server/tool/x").is_none());
176 }
177
178 #[test]
179 fn bracketed_ipv6_hostname_check_uses_bare_host() {
180 let b = bind_key_from_uri("ws://[::1]:8080/path").expect("parses"); assert!(b.loopback, "[::1] is loopback");
182 }
183
184 #[test]
185 fn localhost_authority_with_port_is_loopback() {
186 let b = bind_key_from_uri("http://localhost:8080/api").expect("parses"); assert!(b.loopback, "localhost is loopback");
190 let b = bind_key_from_uri("http://myhost.example:8080").expect("parses"); assert!(!b.loopback, "other hostnames stay non-loopback");
192 }
193}
194
195async fn rollback_started(route_id: &str, handles: &[Arc<dyn StepLifecycle>]) {
206 for handle in handles.iter().rev() {
207 if let Err(e) = handle.shutdown(StepShutdownReason::RouteStop).await {
208 warn!(
209 route_id = %route_id,
210 step = handle.name(),
211 error = %e,
212 "best-effort step shutdown during start rollback failed"
213 );
214 }
215 }
216}
217
218#[async_trait::async_trait]
219impl camel_api::RouteController for DefaultRouteController {
220 async fn start_route(&mut self, route_id: &str) -> Result<(), CamelError> {
221 {
223 let managed = self
224 .routes
225 .get_mut(route_id)
226 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
227
228 let consumer_running = handle_is_running(&managed.consumer_handle);
229 let pipeline_running = handle_is_running(&managed.pipeline_handle);
230 if consumer_running && pipeline_running {
231 return Ok(());
232 }
233 if !consumer_running && pipeline_running {
234 return Err(CamelError::RouteError(format!(
235 "Route '{}' is suspended; use resume_route() to resume, or stop_route() then start_route() for full restart",
236 route_id
237 )));
238 }
239 if consumer_running && !pipeline_running {
240 return Err(CamelError::RouteError(format!(
241 "Route '{}' has inconsistent execution state; stop_route() then retry start_route()",
242 route_id
243 )));
244 }
245 }
246
247 info!(route_id = %route_id, "Starting route");
248
249 let (from_uri, pipeline, concurrency, dispatch_plan) = {
251 let managed = self
252 .routes
253 .get(route_id)
254 .expect("invariant: route must exist after prior existence check"); (
256 managed.from_uri.clone(),
257 Arc::clone(&managed.pipeline),
258 managed.concurrency.clone(),
259 managed.compiled.security_plan.clone(),
260 )
261 };
262
263 if let Some(bind) = bind_key_from_uri(&from_uri) {
269 let owned = self.plans_for_bind(&bind.key);
270 let siblings: Vec<(&str, &RouteSecurityPlan)> =
271 owned.iter().map(|(id, plan)| (id.as_str(), plan)).collect();
272 enforce_bind_exposure_gate(
273 &bind.key,
274 bind.loopback,
275 &siblings,
276 self.bind_acks.acknowledged(&bind.key),
277 )?;
278 }
279
280 let lifecycle_handles: Vec<Arc<dyn StepLifecycle>> = pipeline.load().lifecycle.clone();
286 for (idx, handle) in lifecycle_handles.iter().enumerate() {
287 if let Err(start_err) = handle.start().await {
288 warn!(
289 route_id = %route_id,
290 step = handle.name(),
291 "step start failed; rolling back already-started steps"
292 );
293 rollback_started(route_id, &lifecycle_handles[0..idx]).await;
295 return Err(start_err);
296 }
297 }
298
299 let crash_notifier = self.crash_notifier.clone();
301 let runtime_for_consumer = self.runtime.clone();
302
303 let consumer_component_ctx = Arc::new(ControllerComponentContext::new(
304 Arc::clone(&self.registry),
305 Arc::clone(&self.languages),
306 self.tracer_metrics
307 .clone()
308 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
309 Arc::clone(&self.platform_service),
310 self.health_registry(),
311 Some(route_id.to_string()),
312 ));
313 let consumer_rt: Arc<dyn camel_component_api::RuntimeObservability> =
314 Arc::clone(&consumer_component_ctx) as Arc<_>;
315 let (mut consumer, consumer_concurrency) = match consumer_management::create_route_consumer(
316 consumer_rt,
317 &self.registry,
318 &from_uri,
319 consumer_component_ctx.as_ref(),
320 ) {
321 Ok(v) => v,
322 Err(e) => {
326 rollback_started(route_id, &lifecycle_handles).await;
327 return Err(e);
328 }
329 };
330
331 let effective_concurrency = concurrency.unwrap_or(consumer_concurrency);
333
334 let managed = self
343 .routes
344 .get_mut(route_id)
345 .expect("invariant: route must exist after prior existence check"); deliver_security_context(consumer.as_mut(), &managed.compiled);
347
348 let (tx, mut rx) = mpsc::channel::<ExchangeEnvelope>(256);
350 let consumer_cancel = managed.consumer_cancel_token.child_token();
352 let pipeline_cancel = managed.pipeline_cancel_token.child_token();
353 let drain_in_flight = Arc::clone(&managed.drain_in_flight);
354 let tx_for_storage = tx.clone();
356 let consumer_ctx = ConsumerContext::new(tx, consumer_cancel.clone(), route_id.to_string());
357
358 let split_clone = managed.aggregate_split.clone();
360 if let Some(split) = split_clone {
361 let result = self
362 .start_aggregate_route(
363 route_id,
364 split,
365 consumer,
366 consumer_ctx,
367 rx,
368 crash_notifier,
369 runtime_for_consumer,
370 tx_for_storage,
371 pipeline_cancel,
372 drain_in_flight,
373 )
374 .await;
375 if result.is_err() {
378 if let Some(managed) = self.routes.get_mut(route_id) {
381 managed.consumer_cancel_token.cancel();
382 }
383 rollback_started(route_id, &lifecycle_handles).await;
384 }
385 return result;
386 }
387 let pipeline_cancel_for_cleanup = pipeline_cancel.clone();
393
394 let pipeline_handle = match effective_concurrency {
396 ConcurrencyModel::Concurrent { max } => {
397 let route_id = route_id.to_string();
399 let sem = max.map(|n| Arc::new(tokio::sync::Semaphore::new(n)));
400 tokio::spawn(async move {
401 loop {
402 let permit = match &sem {
405 Some(s) => {
406 let acquired = tokio::select! {
407 p = Arc::clone(s).acquire_owned() => p.expect("semaphore closed"), _ = pipeline_cancel.cancelled() => return,
409 };
410 Some(acquired)
411 }
412 None => None,
413 };
414
415 let envelope = tokio::select! {
416 envelope = rx.recv() => match envelope {
417 Some(e) => e,
418 None => return,
419 },
420 _ = pipeline_cancel.cancelled() => return,
421 };
422 let ExchangeEnvelope {
423 exchange,
424 mut reply_tx,
425 } = envelope;
426 if strict_dispatch_denies(
435 &dispatch_plan,
436 &exchange,
437 &mut reply_tx,
438 route_id.as_str(),
439 ) {
440 continue;
441 }
442 let pipe_ref = Arc::clone(&pipeline);
443 let cancel = pipeline_cancel.clone();
444 let drain_clone = Arc::clone(&drain_in_flight);
445 tokio::spawn(async move {
446 let _permit = permit;
448 let _drain_guard = DrainGuard::new(drain_clone);
449
450 let mut pipe = pipe_ref.load().processor.clone_inner();
452
453 if let Err(e) = ready_with_backoff(&mut pipe, &cancel).await {
455 if let Some(tx) = reply_tx {
456 let _ = tx.send(Err(e));
457 }
458 return;
459 }
460
461 let result = CANCEL_TOKEN
464 .scope(cancel, async move { pipe.call(exchange).await })
465 .await;
466 if let Some(tx) = reply_tx {
467 let _ = tx.send(result);
468 } else if let Err(ref e) = result {
469 error!("Pipeline error: {e}");
471 }
472 });
473 }
474 })
475 }
476 _ => {
482 let route_id = route_id.to_string();
484 tokio::spawn(async move {
485 loop {
486 let envelope = tokio::select! {
488 envelope = rx.recv() => match envelope {
489 Some(e) => e,
490 None => return, },
492 _ = pipeline_cancel.cancelled() => {
493 return;
495 }
496 };
497 let ExchangeEnvelope {
498 exchange,
499 mut reply_tx,
500 } = envelope;
501
502 if strict_dispatch_denies(
505 &dispatch_plan,
506 &exchange,
507 &mut reply_tx,
508 route_id.as_str(),
509 ) {
510 continue;
511 }
512
513 let mut pipeline = pipeline.load().processor.clone_inner();
515
516 if let Err(e) = ready_with_backoff(&mut pipeline, &pipeline_cancel).await {
517 if let Some(tx) = reply_tx {
518 let _ = tx.send(Err(e));
519 }
520 return;
521 }
522
523 let cancel = pipeline_cancel.clone();
529 let _drain_guard = DrainGuard::new(Arc::clone(&drain_in_flight));
530 let result = CANCEL_TOKEN
531 .scope(cancel, async move { pipeline.call(exchange).await })
532 .await;
533 if let Some(tx) = reply_tx {
534 let _ = tx.send(result);
535 } else if let Err(ref e) = result {
536 error!("Pipeline error: {e}");
538 }
539 }
540 })
541 }
542 };
543 #[cfg(test)]
544 emit_start_route_event("pipeline_spawned", route_id);
545
546 let (consumer_handle, startup_rx) = consumer_management::spawn_consumer_task(
549 route_id.to_string(),
550 consumer,
551 consumer_ctx,
552 crash_notifier,
553 runtime_for_consumer,
554 false,
555 );
556 #[cfg(test)]
557 emit_start_route_event("consumer_spawned", route_id);
558
559 match consumer_management::await_consumer_startup(startup_rx, "startup").await {
564 Ok(()) => {}
565 Err(e) => {
566 consumer_handle.abort();
573 pipeline_cancel_for_cleanup.cancel();
574 consumer_cancel.cancel();
577 rollback_started(route_id, &lifecycle_handles).await;
578 return Err(e);
579 }
580 }
581
582 let managed = self
584 .routes
585 .get_mut(route_id)
586 .expect("invariant: route must exist after prior existence check"); managed.consumer_handle = Some(consumer_handle);
588 managed.pipeline_handle = Some(pipeline_handle);
589 managed.channel_sender = Some(tx_for_storage);
590
591 info!(route_id = %route_id, "Route started");
592 self.health_registry().mark_route_started(route_id);
593 Ok(())
594 }
595
596 async fn stop_route(&mut self, route_id: &str) -> Result<(), CamelError> {
597 self.stop_route_internal(route_id).await?;
598 self.health_registry().mark_route_stopped(route_id);
599 Ok(())
600 }
601
602 async fn restart_route(&mut self, route_id: &str) -> Result<(), CamelError> {
603 self.stop_route(route_id).await?;
604 tokio::time::sleep(Duration::from_millis(100)).await;
605 self.start_route(route_id).await
606 }
607
608 async fn suspend_route(&mut self, route_id: &str) -> Result<(), CamelError> {
609 let managed = self
611 .routes
612 .get_mut(route_id)
613 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
614
615 let consumer_running = handle_is_running(&managed.consumer_handle);
616 let pipeline_running = handle_is_running(&managed.pipeline_handle);
617
618 if !consumer_running || !pipeline_running {
620 return Err(CamelError::RouteError(format!(
621 "Cannot suspend route '{}' with execution lifecycle {}",
622 route_id,
623 inferred_lifecycle_label(managed)
624 )));
625 }
626
627 info!(route_id = %route_id, "Suspending route (consumer only, keeping pipeline)");
628
629 let managed = self
631 .routes
632 .get_mut(route_id)
633 .expect("invariant: route must exist after prior existence check"); managed.consumer_cancel_token.cancel();
635
636 let managed = self
638 .routes
639 .get_mut(route_id)
640 .expect("invariant: route must exist after prior existence check"); let consumer_handle = managed.consumer_handle.take();
642
643 let timeout_result = tokio::time::timeout(DEFAULT_SHUTDOWN_TIMEOUT, async {
645 if let Some(handle) = consumer_handle {
646 let _ = handle.await;
647 }
648 })
649 .await;
650
651 if timeout_result.is_err() {
652 warn!(route_id = %route_id, "Consumer shutdown timed out during suspend");
653 }
654
655 let managed = self
657 .routes
658 .get_mut(route_id)
659 .expect("invariant: route must exist after prior existence check"); managed.consumer_cancel_token = CancellationToken::new();
663
664 info!(route_id = %route_id, "Route suspended (pipeline still running)");
665 self.health_registry().mark_route_stopped(route_id);
666 Ok(())
667 }
668
669 async fn resume_route(&mut self, route_id: &str) -> Result<(), CamelError> {
670 let managed = self
672 .routes
673 .get(route_id)
674 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
675
676 let consumer_running = handle_is_running(&managed.consumer_handle);
677 let pipeline_running = handle_is_running(&managed.pipeline_handle);
678 if consumer_running || !pipeline_running {
679 return Err(CamelError::RouteError(format!(
680 "Cannot resume route '{}' with execution lifecycle {} (expected Suspended)",
681 route_id,
682 inferred_lifecycle_label(managed)
683 )));
684 }
685
686 let sender = managed.channel_sender.clone().ok_or_else(|| {
688 CamelError::RouteError("Suspended route has no channel sender".into())
689 })?;
690
691 let from_uri = managed.from_uri.clone();
693
694 if let Some(bind) = bind_key_from_uri(&from_uri) {
696 let owned = self.plans_for_bind(&bind.key);
697 let siblings: Vec<(&str, &RouteSecurityPlan)> =
698 owned.iter().map(|(id, plan)| (id.as_str(), plan)).collect();
699 enforce_bind_exposure_gate(
700 &bind.key,
701 bind.loopback,
702 &siblings,
703 self.bind_acks.acknowledged(&bind.key),
704 )?;
705 }
706
707 info!(route_id = %route_id, "Resuming route (spawning consumer only)");
708
709 let consumer_component_ctx = Arc::new(ControllerComponentContext::new(
710 Arc::clone(&self.registry),
711 Arc::clone(&self.languages),
712 self.tracer_metrics
713 .clone()
714 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
715 Arc::clone(&self.platform_service),
716 self.health_registry(),
717 Some(route_id.to_string()),
718 ));
719 let consumer_rt: Arc<dyn camel_component_api::RuntimeObservability> =
720 Arc::clone(&consumer_component_ctx) as Arc<_>;
721 let (mut consumer, _) = consumer_management::create_route_consumer(
722 consumer_rt,
723 &self.registry,
724 &from_uri,
725 consumer_component_ctx.as_ref(),
726 )?;
727
728 let managed = self
731 .routes
732 .get(route_id)
733 .expect("invariant: route must exist after prior existence check"); deliver_security_context(consumer.as_mut(), &managed.compiled);
735
736 let managed = self
738 .routes
739 .get_mut(route_id)
740 .expect("invariant: route must exist after prior existence check"); let consumer_cancel = managed.consumer_cancel_token.child_token();
744
745 let crash_notifier = self.crash_notifier.clone();
746 let runtime_for_consumer = self.runtime.clone();
747
748 let consumer_ctx =
750 ConsumerContext::new(sender, consumer_cancel.clone(), route_id.to_string());
751
752 let (consumer_handle, startup_rx) = consumer_management::spawn_consumer_task(
754 route_id.to_string(),
755 consumer,
756 consumer_ctx,
757 crash_notifier,
758 runtime_for_consumer,
759 true,
760 );
761
762 consumer_management::await_consumer_startup(startup_rx, "resume").await?;
765
766 let managed = self
768 .routes
769 .get_mut(route_id)
770 .expect("invariant: route must exist after prior existence check"); managed.consumer_handle = Some(consumer_handle);
772
773 info!(route_id = %route_id, "Route resumed");
774 self.health_registry().mark_route_started(route_id);
775 Ok(())
776 }
777
778 async fn start_all_routes(&mut self) -> Result<(), CamelError> {
779 let route_ids: Vec<String> = {
782 let pairs = self.routes.auto_startup_sorted();
783 pairs.into_iter().map(|(id, _)| id).collect()
784 };
785
786 info!("Starting {} auto-startup routes", route_ids.len());
787
788 let mut errors: Vec<String> = Vec::new();
790 for route_id in route_ids {
791 if let Err(e) = self.start_route(&route_id).await {
792 errors.push(format!("Route '{}': {}", route_id, e));
793 }
794 }
795
796 if !errors.is_empty() {
797 return Err(CamelError::RouteError(format!(
798 "Failed to start routes: {}",
799 errors.join(", ")
800 )));
801 }
802
803 info!("All auto-startup routes started");
804 Ok(())
805 }
806
807 async fn stop_all_routes(&mut self) -> Result<(), CamelError> {
808 let route_ids: Vec<String> = {
810 let pairs = self.routes.shutdown_sorted();
811 pairs.into_iter().map(|(id, _)| id).collect()
812 };
813
814 info!("Stopping {} routes", route_ids.len());
815
816 for route_id in route_ids {
817 let _ = self.stop_route(&route_id).await;
818 }
819
820 info!("All routes stopped");
821 Ok(())
822 }
823}
824
825#[cfg(test)]
826#[path = "route_controller_trait_tests.rs"]
827mod bind_exposure_gate;