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::{ConcurrencyModel, ConsumerContext, consumer::ExchangeEnvelope};
17
18use crate::lifecycle::adapters::consumer_management;
19use crate::lifecycle::adapters::controller_component_context::ControllerComponentContext;
20use crate::lifecycle::adapters::route_compiler::CANCEL_TOKEN;
21use crate::lifecycle::adapters::route_controller::DefaultRouteController;
22#[cfg(test)]
23use crate::lifecycle::adapters::route_helpers::emit_start_route_event;
24use crate::lifecycle::adapters::route_helpers::{
25 DrainGuard, handle_is_running, inferred_lifecycle_label, ready_with_backoff,
26};
27use crate::lifecycle::adapters::route_registry::DEFAULT_SHUTDOWN_TIMEOUT;
28
29pub use camel_auth::bind_gate::{BindExposureAcks, enforce_bind_exposure_gate};
38
39pub(super) struct BindKey {
44 pub(super) key: String,
45 pub(super) loopback: bool,
46}
47
48pub(super) fn bind_key_from_uri(uri: &str) -> Option<BindKey> {
49 let scheme = uri.split(':').next()?;
50 if !matches!(scheme, "http" | "https" | "ws" | "wss" | "grpc") {
51 return None;
52 }
53 let authority = uri.split("://").nth(1)?;
54 let authority = authority.split('/').next()?;
55 if authority.is_empty() {
56 return None;
57 }
58 if let Ok(addr) = authority.parse::<std::net::SocketAddr>() {
59 return Some(BindKey {
60 key: addr.to_string(),
61 loopback: addr.ip().is_loopback(),
62 });
63 }
64 let host = authority
69 .rsplit_once(':')
70 .map(|(h, _)| h)
71 .unwrap_or(authority)
72 .trim_matches(['[', ']']);
73 let loopback = host.eq_ignore_ascii_case("localhost");
74 Some(BindKey {
75 key: authority.to_string(),
76 loopback,
77 })
78}
79
80fn strict_dispatch_denies(
88 dispatch_plan: &Option<camel_api::security_policy::RouteSecurityPlan>,
89 exchange: &camel_api::Exchange,
90 reply_tx: &mut Option<tokio::sync::oneshot::Sender<Result<camel_api::Exchange, CamelError>>>,
91 route_id: &str,
92) -> bool {
93 if let Some(plan) = dispatch_plan.as_ref()
94 && let Err(denial) = camel_auth::enforce_dispatch(plan, exchange)
95 {
96 if let Some(tx) = reply_tx.take() {
97 let _ = tx.send(Err(denial));
98 } else {
99 warn!(
101 route_id = %route_id,
102 error = %denial,
103 "dispatch denied: no kernel carrier on Exchange"
104 );
105 }
106 return true;
107 }
108 false
109}
110
111#[cfg(test)]
112mod bind_key_tests {
113 use super::bind_key_from_uri;
114
115 #[test]
116 fn https_and_wss_listeners_gate() {
117 assert_eq!(
118 bind_key_from_uri("https://0.0.0.0:8443/api").map(|b| b.key),
119 Some("0.0.0.0:8443".to_string())
120 );
121 assert_eq!(
122 bind_key_from_uri("wss://0.0.0.0:9000").map(|b| b.key),
123 Some("0.0.0.0:9000".to_string())
124 );
125 }
126
127 #[test]
128 fn non_listener_schemes_skip() {
129 assert!(bind_key_from_uri("timer:tick?period=1s").is_none());
130 assert!(bind_key_from_uri("mcp:server/tool/x").is_none());
131 }
132
133 #[test]
134 fn bracketed_ipv6_hostname_check_uses_bare_host() {
135 let b = bind_key_from_uri("ws://[::1]:8080/path").expect("parses"); assert!(b.loopback, "[::1] is loopback");
137 }
138
139 #[test]
140 fn localhost_authority_with_port_is_loopback() {
141 let b = bind_key_from_uri("http://localhost:8080/api").expect("parses"); assert!(b.loopback, "localhost is loopback");
145 let b = bind_key_from_uri("http://myhost.example:8080").expect("parses"); assert!(!b.loopback, "other hostnames stay non-loopback");
147 }
148}
149
150async fn rollback_started(route_id: &str, handles: &[Arc<dyn StepLifecycle>]) {
161 for handle in handles.iter().rev() {
162 if let Err(e) = handle.shutdown(StepShutdownReason::RouteStop).await {
163 warn!(
164 route_id = %route_id,
165 step = handle.name(),
166 error = %e,
167 "best-effort step shutdown during start rollback failed"
168 );
169 }
170 }
171}
172
173#[async_trait::async_trait]
174impl camel_api::RouteController for DefaultRouteController {
175 async fn start_route(&mut self, route_id: &str) -> Result<(), CamelError> {
176 {
178 let managed = self
179 .routes
180 .get_mut(route_id)
181 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
182
183 let consumer_running = handle_is_running(&managed.consumer_handle);
184 let pipeline_running = handle_is_running(&managed.pipeline_handle);
185 if consumer_running && pipeline_running {
186 return Ok(());
187 }
188 if !consumer_running && pipeline_running {
189 return Err(CamelError::RouteError(format!(
190 "Route '{}' is suspended; use resume_route() to resume, or stop_route() then start_route() for full restart",
191 route_id
192 )));
193 }
194 if consumer_running && !pipeline_running {
195 return Err(CamelError::RouteError(format!(
196 "Route '{}' has inconsistent execution state; stop_route() then retry start_route()",
197 route_id
198 )));
199 }
200 }
201
202 info!(route_id = %route_id, "Starting route");
203
204 let (from_uri, pipeline, concurrency, dispatch_plan) = {
206 let managed = self
207 .routes
208 .get(route_id)
209 .expect("invariant: route must exist after prior existence check"); (
211 managed.from_uri.clone(),
212 Arc::clone(&managed.pipeline),
213 managed.concurrency.clone(),
214 managed.compiled.security_plan.clone(),
215 )
216 };
217
218 if let Some(bind) = bind_key_from_uri(&from_uri) {
224 let owned = self.plans_for_bind(&bind.key);
225 let siblings: Vec<(&str, &RouteSecurityPlan)> =
226 owned.iter().map(|(id, plan)| (id.as_str(), plan)).collect();
227 enforce_bind_exposure_gate(
228 &bind.key,
229 bind.loopback,
230 &siblings,
231 self.bind_acks.acknowledged(&bind.key),
232 )?;
233 }
234
235 let lifecycle_handles: Vec<Arc<dyn StepLifecycle>> = pipeline.load().lifecycle.clone();
241 for (idx, handle) in lifecycle_handles.iter().enumerate() {
242 if let Err(start_err) = handle.start().await {
243 warn!(
244 route_id = %route_id,
245 step = handle.name(),
246 "step start failed; rolling back already-started steps"
247 );
248 rollback_started(route_id, &lifecycle_handles[0..idx]).await;
250 return Err(start_err);
251 }
252 }
253
254 let crash_notifier = self.crash_notifier.clone();
256 let runtime_for_consumer = self.runtime.clone();
257
258 let consumer_component_ctx = Arc::new(ControllerComponentContext::new(
259 Arc::clone(&self.registry),
260 Arc::clone(&self.languages),
261 self.tracer_metrics
262 .clone()
263 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
264 Arc::clone(&self.platform_service),
265 self.health_registry(),
266 Some(route_id.to_string()),
267 ));
268 let consumer_rt: Arc<dyn camel_component_api::RuntimeObservability> =
269 Arc::clone(&consumer_component_ctx) as Arc<_>;
270 let (mut consumer, consumer_concurrency) = match consumer_management::create_route_consumer(
271 consumer_rt,
272 &self.registry,
273 &from_uri,
274 consumer_component_ctx.as_ref(),
275 ) {
276 Ok(v) => v,
277 Err(e) => {
281 rollback_started(route_id, &lifecycle_handles).await;
282 return Err(e);
283 }
284 };
285
286 let effective_concurrency = concurrency.unwrap_or(consumer_concurrency);
288
289 let managed = self
291 .routes
292 .get_mut(route_id)
293 .expect("invariant: route must exist after prior existence check"); if let (Some(sp_config), Some(authenticator)) = (
297 managed.compiled.security_policy.as_ref(),
298 managed.compiled.security_authenticator.as_ref(),
299 ) {
300 use camel_component_api::SecurityContext;
301 let mut sec_ctx =
302 SecurityContext::from_arc(Arc::clone(&sp_config.policy), Arc::clone(authenticator))
303 .with_credential_sources(sp_config.credential_sources.clone());
304 if let Some(registry) = &managed.compiled.provider_registry {
308 sec_ctx = sec_ctx.with_providers(Arc::clone(registry));
309 }
310 if let Some(plan) = &managed.compiled.security_plan {
313 sec_ctx = sec_ctx.with_plan(plan.clone());
314 }
315 consumer.set_security_context(sec_ctx);
316 }
317
318 let (tx, mut rx) = mpsc::channel::<ExchangeEnvelope>(256);
320 let consumer_cancel = managed.consumer_cancel_token.child_token();
322 let pipeline_cancel = managed.pipeline_cancel_token.child_token();
323 let drain_in_flight = Arc::clone(&managed.drain_in_flight);
324 let tx_for_storage = tx.clone();
326 let consumer_ctx = ConsumerContext::new(tx, consumer_cancel.clone(), route_id.to_string());
327
328 let split_clone = managed.aggregate_split.clone();
330 if let Some(split) = split_clone {
331 let result = self
332 .start_aggregate_route(
333 route_id,
334 split,
335 consumer,
336 consumer_ctx,
337 rx,
338 crash_notifier,
339 runtime_for_consumer,
340 tx_for_storage,
341 pipeline_cancel,
342 drain_in_flight,
343 )
344 .await;
345 if result.is_err() {
348 if let Some(managed) = self.routes.get_mut(route_id) {
351 managed.consumer_cancel_token.cancel();
352 }
353 rollback_started(route_id, &lifecycle_handles).await;
354 }
355 return result;
356 }
357 let pipeline_cancel_for_cleanup = pipeline_cancel.clone();
363
364 let pipeline_handle = match effective_concurrency {
366 ConcurrencyModel::Concurrent { max } => {
367 let route_id = route_id.to_string();
369 let sem = max.map(|n| Arc::new(tokio::sync::Semaphore::new(n)));
370 tokio::spawn(async move {
371 loop {
372 let permit = match &sem {
375 Some(s) => {
376 let acquired = tokio::select! {
377 p = Arc::clone(s).acquire_owned() => p.expect("semaphore closed"), _ = pipeline_cancel.cancelled() => return,
379 };
380 Some(acquired)
381 }
382 None => None,
383 };
384
385 let envelope = tokio::select! {
386 envelope = rx.recv() => match envelope {
387 Some(e) => e,
388 None => return,
389 },
390 _ = pipeline_cancel.cancelled() => return,
391 };
392 let ExchangeEnvelope {
393 exchange,
394 mut reply_tx,
395 } = envelope;
396 if strict_dispatch_denies(
405 &dispatch_plan,
406 &exchange,
407 &mut reply_tx,
408 route_id.as_str(),
409 ) {
410 continue;
411 }
412 let pipe_ref = Arc::clone(&pipeline);
413 let cancel = pipeline_cancel.clone();
414 let drain_clone = Arc::clone(&drain_in_flight);
415 tokio::spawn(async move {
416 let _permit = permit;
418 let _drain_guard = DrainGuard::new(drain_clone);
419
420 let mut pipe = pipe_ref.load().processor.clone_inner();
422
423 if let Err(e) = ready_with_backoff(&mut pipe, &cancel).await {
425 if let Some(tx) = reply_tx {
426 let _ = tx.send(Err(e));
427 }
428 return;
429 }
430
431 let result = CANCEL_TOKEN
434 .scope(cancel, async move { pipe.call(exchange).await })
435 .await;
436 if let Some(tx) = reply_tx {
437 let _ = tx.send(result);
438 } else if let Err(ref e) = result {
439 error!("Pipeline error: {e}");
441 }
442 });
443 }
444 })
445 }
446 _ => {
452 let route_id = route_id.to_string();
454 tokio::spawn(async move {
455 loop {
456 let envelope = tokio::select! {
458 envelope = rx.recv() => match envelope {
459 Some(e) => e,
460 None => return, },
462 _ = pipeline_cancel.cancelled() => {
463 return;
465 }
466 };
467 let ExchangeEnvelope {
468 exchange,
469 mut reply_tx,
470 } = envelope;
471
472 if strict_dispatch_denies(
475 &dispatch_plan,
476 &exchange,
477 &mut reply_tx,
478 route_id.as_str(),
479 ) {
480 continue;
481 }
482
483 let mut pipeline = pipeline.load().processor.clone_inner();
485
486 if let Err(e) = ready_with_backoff(&mut pipeline, &pipeline_cancel).await {
487 if let Some(tx) = reply_tx {
488 let _ = tx.send(Err(e));
489 }
490 return;
491 }
492
493 let cancel = pipeline_cancel.clone();
499 let _drain_guard = DrainGuard::new(Arc::clone(&drain_in_flight));
500 let result = CANCEL_TOKEN
501 .scope(cancel, async move { pipeline.call(exchange).await })
502 .await;
503 if let Some(tx) = reply_tx {
504 let _ = tx.send(result);
505 } else if let Err(ref e) = result {
506 error!("Pipeline error: {e}");
508 }
509 }
510 })
511 }
512 };
513 #[cfg(test)]
514 emit_start_route_event("pipeline_spawned");
515
516 let (consumer_handle, startup_rx) = consumer_management::spawn_consumer_task(
519 route_id.to_string(),
520 consumer,
521 consumer_ctx,
522 crash_notifier,
523 runtime_for_consumer,
524 false,
525 );
526 #[cfg(test)]
527 emit_start_route_event("consumer_spawned");
528
529 match consumer_management::await_consumer_startup(startup_rx, "startup").await {
534 Ok(()) => {}
535 Err(e) => {
536 consumer_handle.abort();
543 pipeline_cancel_for_cleanup.cancel();
544 consumer_cancel.cancel();
547 rollback_started(route_id, &lifecycle_handles).await;
548 return Err(e);
549 }
550 }
551
552 let managed = self
554 .routes
555 .get_mut(route_id)
556 .expect("invariant: route must exist after prior existence check"); managed.consumer_handle = Some(consumer_handle);
558 managed.pipeline_handle = Some(pipeline_handle);
559 managed.channel_sender = Some(tx_for_storage);
560
561 info!(route_id = %route_id, "Route started");
562 self.health_registry().mark_route_started(route_id);
563 Ok(())
564 }
565
566 async fn stop_route(&mut self, route_id: &str) -> Result<(), CamelError> {
567 self.stop_route_internal(route_id).await?;
568 self.health_registry().mark_route_stopped(route_id);
569 Ok(())
570 }
571
572 async fn restart_route(&mut self, route_id: &str) -> Result<(), CamelError> {
573 self.stop_route(route_id).await?;
574 tokio::time::sleep(Duration::from_millis(100)).await;
575 self.start_route(route_id).await
576 }
577
578 async fn suspend_route(&mut self, route_id: &str) -> Result<(), CamelError> {
579 let managed = self
581 .routes
582 .get_mut(route_id)
583 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
584
585 let consumer_running = handle_is_running(&managed.consumer_handle);
586 let pipeline_running = handle_is_running(&managed.pipeline_handle);
587
588 if !consumer_running || !pipeline_running {
590 return Err(CamelError::RouteError(format!(
591 "Cannot suspend route '{}' with execution lifecycle {}",
592 route_id,
593 inferred_lifecycle_label(managed)
594 )));
595 }
596
597 info!(route_id = %route_id, "Suspending route (consumer only, keeping pipeline)");
598
599 let managed = self
601 .routes
602 .get_mut(route_id)
603 .expect("invariant: route must exist after prior existence check"); managed.consumer_cancel_token.cancel();
605
606 let managed = self
608 .routes
609 .get_mut(route_id)
610 .expect("invariant: route must exist after prior existence check"); let consumer_handle = managed.consumer_handle.take();
612
613 let timeout_result = tokio::time::timeout(DEFAULT_SHUTDOWN_TIMEOUT, async {
615 if let Some(handle) = consumer_handle {
616 let _ = handle.await;
617 }
618 })
619 .await;
620
621 if timeout_result.is_err() {
622 warn!(route_id = %route_id, "Consumer shutdown timed out during suspend");
623 }
624
625 let managed = self
627 .routes
628 .get_mut(route_id)
629 .expect("invariant: route must exist after prior existence check"); managed.consumer_cancel_token = CancellationToken::new();
633
634 info!(route_id = %route_id, "Route suspended (pipeline still running)");
635 self.health_registry().mark_route_stopped(route_id);
636 Ok(())
637 }
638
639 async fn resume_route(&mut self, route_id: &str) -> Result<(), CamelError> {
640 let managed = self
642 .routes
643 .get(route_id)
644 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
645
646 let consumer_running = handle_is_running(&managed.consumer_handle);
647 let pipeline_running = handle_is_running(&managed.pipeline_handle);
648 if consumer_running || !pipeline_running {
649 return Err(CamelError::RouteError(format!(
650 "Cannot resume route '{}' with execution lifecycle {} (expected Suspended)",
651 route_id,
652 inferred_lifecycle_label(managed)
653 )));
654 }
655
656 let sender = managed.channel_sender.clone().ok_or_else(|| {
658 CamelError::RouteError("Suspended route has no channel sender".into())
659 })?;
660
661 let from_uri = managed.from_uri.clone();
663
664 if let Some(bind) = bind_key_from_uri(&from_uri) {
666 let owned = self.plans_for_bind(&bind.key);
667 let siblings: Vec<(&str, &RouteSecurityPlan)> =
668 owned.iter().map(|(id, plan)| (id.as_str(), plan)).collect();
669 enforce_bind_exposure_gate(
670 &bind.key,
671 bind.loopback,
672 &siblings,
673 self.bind_acks.acknowledged(&bind.key),
674 )?;
675 }
676
677 info!(route_id = %route_id, "Resuming route (spawning consumer only)");
678
679 let consumer_component_ctx = Arc::new(ControllerComponentContext::new(
680 Arc::clone(&self.registry),
681 Arc::clone(&self.languages),
682 self.tracer_metrics
683 .clone()
684 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
685 Arc::clone(&self.platform_service),
686 self.health_registry(),
687 Some(route_id.to_string()),
688 ));
689 let consumer_rt: Arc<dyn camel_component_api::RuntimeObservability> =
690 Arc::clone(&consumer_component_ctx) as Arc<_>;
691 let (mut consumer, _) = consumer_management::create_route_consumer(
692 consumer_rt,
693 &self.registry,
694 &from_uri,
695 consumer_component_ctx.as_ref(),
696 )?;
697
698 let managed = self
700 .routes
701 .get(route_id)
702 .expect("invariant: route must exist after prior existence check"); if let (Some(sp_config), Some(authenticator)) = (
704 managed.compiled.security_policy.as_ref(),
705 managed.compiled.security_authenticator.as_ref(),
706 ) {
707 use camel_component_api::SecurityContext;
708 let mut sec_ctx =
709 SecurityContext::from_arc(Arc::clone(&sp_config.policy), Arc::clone(authenticator))
710 .with_credential_sources(sp_config.credential_sources.clone());
711 if let Some(registry) = &managed.compiled.provider_registry {
713 sec_ctx = sec_ctx.with_providers(Arc::clone(registry));
714 }
715 if let Some(plan) = &managed.compiled.security_plan {
717 sec_ctx = sec_ctx.with_plan(plan.clone());
718 }
719 consumer.set_security_context(sec_ctx);
720 }
721
722 let managed = self
724 .routes
725 .get_mut(route_id)
726 .expect("invariant: route must exist after prior existence check"); let consumer_cancel = managed.consumer_cancel_token.child_token();
730
731 let crash_notifier = self.crash_notifier.clone();
732 let runtime_for_consumer = self.runtime.clone();
733
734 let consumer_ctx =
736 ConsumerContext::new(sender, consumer_cancel.clone(), route_id.to_string());
737
738 let (consumer_handle, startup_rx) = consumer_management::spawn_consumer_task(
740 route_id.to_string(),
741 consumer,
742 consumer_ctx,
743 crash_notifier,
744 runtime_for_consumer,
745 true,
746 );
747
748 consumer_management::await_consumer_startup(startup_rx, "resume").await?;
751
752 let managed = self
754 .routes
755 .get_mut(route_id)
756 .expect("invariant: route must exist after prior existence check"); managed.consumer_handle = Some(consumer_handle);
758
759 info!(route_id = %route_id, "Route resumed");
760 self.health_registry().mark_route_started(route_id);
761 Ok(())
762 }
763
764 async fn start_all_routes(&mut self) -> Result<(), CamelError> {
765 let route_ids: Vec<String> = {
768 let pairs = self.routes.auto_startup_sorted();
769 pairs.into_iter().map(|(id, _)| id).collect()
770 };
771
772 info!("Starting {} auto-startup routes", route_ids.len());
773
774 let mut errors: Vec<String> = Vec::new();
776 for route_id in route_ids {
777 if let Err(e) = self.start_route(&route_id).await {
778 errors.push(format!("Route '{}': {}", route_id, e));
779 }
780 }
781
782 if !errors.is_empty() {
783 return Err(CamelError::RouteError(format!(
784 "Failed to start routes: {}",
785 errors.join(", ")
786 )));
787 }
788
789 info!("All auto-startup routes started");
790 Ok(())
791 }
792
793 async fn stop_all_routes(&mut self) -> Result<(), CamelError> {
794 let route_ids: Vec<String> = {
796 let pairs = self.routes.shutdown_sorted();
797 pairs.into_iter().map(|(id, _)| id).collect()
798 };
799
800 info!("Stopping {} routes", route_ids.len());
801
802 for route_id in route_ids {
803 let _ = self.stop_route(&route_id).await;
804 }
805
806 info!("All routes stopped");
807 Ok(())
808 }
809}
810
811#[cfg(test)]
812#[path = "route_controller_trait_tests.rs"]
813mod bind_exposure_gate;