1use std::collections::HashMap;
7use std::sync::{Arc, Weak};
8use std::time::Duration;
9
10use tokio::sync::mpsc;
11use tokio_util::sync::CancellationToken;
12use tower::{Layer, ServiceExt};
13use tracing::{debug, info, warn};
14
15use super::route_controller_trait::{
16 BindExposureAcks, bind_key_from_uri, enforce_bind_exposure_gate,
17};
18use camel_api::error_handler::ErrorHandlerConfig;
19use camel_api::metrics::MetricsCollector;
20#[allow(unused_imports)]
21use camel_api::{
22 BoxProcessor, CamelError, Exchange, FunctionInvoker, IdentityProcessor, NoOpMetrics,
23 NoopPlatformService, PlatformService, ProducerContext, RouteController, RuntimeHandle,
24 StepLifecycle,
25};
26use camel_component_api::{Consumer, ConsumerContext, consumer::ExchangeEnvelope};
27use camel_processor::aggregator::AggregatorService;
28pub use camel_processor::aggregator::SharedLanguageRegistry;
29
30use crate::health_registry::HealthCheckRegistry;
31use crate::intercept::InterceptRules;
32use crate::lifecycle::adapters::controller_component_context::ControllerComponentContext;
33use crate::lifecycle::adapters::route_compiler_ext::{
34 RouteCompilerExt, build_eh_config_pipeline, transport_from_uri,
35};
36use crate::lifecycle::adapters::route_helpers::{
37 AggregateSplitInfo, CrashNotification, ManagedRoute, assert_no_mixed_top_level_splits,
38 handle_is_running, inferred_lifecycle_label, is_pending,
39};
40#[cfg(test)]
41pub(super) use crate::lifecycle::adapters::route_helpers::{
42 emit_start_route_event, set_start_route_event_hook,
43};
44use crate::lifecycle::adapters::route_registry::RouteRegistry;
45use crate::lifecycle::adapters::route_runtime_state;
46use crate::lifecycle::adapters::step_compilers::CompiledStep;
47use crate::lifecycle::application::route_definition::{BuilderStep, RouteDefinition};
48pub(crate) use crate::lifecycle::domain::CompiledPipeline;
49use crate::shared::components::domain::Registry;
50use crate::shared::observability::domain::{DetailLevel, TracerConfig};
51use camel_bean::BeanRegistry;
52
53pub struct DefaultRouteController {
61 pub(super) routes: RouteRegistry,
63 pub(super) registry: Arc<std::sync::Mutex<Registry>>,
65 pub(super) languages: SharedLanguageRegistry,
67 pub(super) beans: Arc<std::sync::Mutex<BeanRegistry>>,
69 pub(super) runtime: Option<Weak<dyn RuntimeHandle>>,
71 pub(super) global_error_handler: Option<ErrorHandlerConfig>,
73 pub(super) crash_notifier: Option<mpsc::Sender<CrashNotification>>,
75 pub(super) tracing_enabled: bool,
77 pub(super) tracer_detail_level: DetailLevel,
79 pub(super) tracer_metrics: Option<Arc<dyn MetricsCollector>>,
81 pub(super) platform_service: Arc<dyn PlatformService>,
82 pub(super) function_invoker: Option<Arc<dyn FunctionInvoker>>,
83 pub(super) health_registry: Option<Arc<HealthCheckRegistry>>,
84 pub(super) idempotent_repositories: crate::SharedIdempotentRegistry,
88 pub(super) claim_check_repositories: crate::SharedClaimCheckRegistry,
89 pub(super) cache_repositories: crate::SharedCacheRegistry,
90 pub(super) prepared_staging: HashMap<String, ManagedRoute>,
95 pub(super) endpoint_index: super::endpoint_index::EndpointIndex,
97 pub(super) bind_acks: super::route_controller_trait::BindExposureAcks,
100 pub(super) intercept: InterceptRules,
102 pub(super) frozen: bool,
106}
107
108impl DefaultRouteController {
109 pub(super) fn health_registry(&self) -> Arc<HealthCheckRegistry> {
110 self.health_registry.clone().unwrap_or_else(|| {
111 debug!("health_registry not configured — creating isolated fallback");
112 Arc::new(HealthCheckRegistry::new(Duration::from_secs(5)))
113 })
114 }
115
116 pub fn new(
118 registry: Arc<std::sync::Mutex<Registry>>,
119 platform_service: Arc<dyn PlatformService>,
120 ) -> Self {
121 Self::with_beans_and_platform_service(
122 registry,
123 Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
124 platform_service,
125 )
126 }
127
128 pub fn with_beans(
130 registry: Arc<std::sync::Mutex<Registry>>,
131 beans: Arc<std::sync::Mutex<BeanRegistry>>,
132 ) -> Self {
133 Self::with_beans_and_platform_service(
134 registry,
135 beans,
136 Arc::new(NoopPlatformService::default()),
137 )
138 }
139
140 fn with_beans_and_platform_service(
141 registry: Arc<std::sync::Mutex<Registry>>,
142 beans: Arc<std::sync::Mutex<BeanRegistry>>,
143 platform_service: Arc<dyn PlatformService>,
144 ) -> Self {
145 Self {
146 routes: RouteRegistry::new(),
147 registry,
148 languages: Arc::new(std::sync::Mutex::new(HashMap::new())),
149 beans,
150 runtime: None,
151 global_error_handler: None,
152 crash_notifier: None,
153 tracing_enabled: false,
154 tracer_detail_level: DetailLevel::Minimal,
155 tracer_metrics: None,
156 platform_service,
157 function_invoker: None,
158 health_registry: None,
159 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
160 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
161 cache_repositories: Arc::new(crate::CacheRegistry::new()),
162 prepared_staging: HashMap::new(),
163 endpoint_index: super::endpoint_index::EndpointIndex::new(),
164 bind_acks: Default::default(),
165 intercept: InterceptRules::default(),
166 frozen: false,
167 }
168 }
169
170 pub fn with_languages(
172 registry: Arc<std::sync::Mutex<Registry>>,
173 languages: SharedLanguageRegistry,
174 platform_service: Arc<dyn PlatformService>,
175 ) -> Self {
176 Self {
177 routes: RouteRegistry::new(),
178 registry,
179 languages,
180 beans: Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
181 runtime: None,
182 global_error_handler: None,
183 crash_notifier: None,
184 tracing_enabled: false,
185 tracer_detail_level: DetailLevel::Minimal,
186 tracer_metrics: None,
187 platform_service,
188 function_invoker: None,
189 health_registry: None,
190 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
191 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
192 cache_repositories: Arc::new(crate::CacheRegistry::new()),
193 prepared_staging: HashMap::new(),
194 endpoint_index: super::endpoint_index::EndpointIndex::new(),
195 bind_acks: Default::default(),
196 intercept: InterceptRules::default(),
197 frozen: false,
198 }
199 }
200
201 pub fn with_languages_and_beans(
202 registry: Arc<std::sync::Mutex<Registry>>,
203 languages: SharedLanguageRegistry,
204 platform_service: Arc<dyn PlatformService>,
205 beans: Arc<std::sync::Mutex<BeanRegistry>>,
206 ) -> Self {
207 Self {
208 routes: RouteRegistry::new(),
209 registry,
210 languages,
211 beans,
212 runtime: None,
213 global_error_handler: None,
214 crash_notifier: None,
215 tracing_enabled: false,
216 tracer_detail_level: DetailLevel::Minimal,
217 tracer_metrics: None,
218 platform_service,
219 function_invoker: None,
220 health_registry: None,
221 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
222 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
223 cache_repositories: Arc::new(crate::CacheRegistry::new()),
224 prepared_staging: HashMap::new(),
225 endpoint_index: super::endpoint_index::EndpointIndex::new(),
226 bind_acks: Default::default(),
227 intercept: InterceptRules::default(),
228 frozen: false,
229 }
230 }
231
232 pub fn with_function_invoker(mut self, function_invoker: Arc<dyn FunctionInvoker>) -> Self {
233 self.function_invoker = Some(function_invoker);
234 self
235 }
236
237 pub(crate) fn set_idempotent_repositories(
238 &mut self,
239 repositories: crate::SharedIdempotentRegistry,
240 ) {
241 self.idempotent_repositories = repositories;
242 }
243
244 pub(crate) fn set_claim_check_repositories(
245 &mut self,
246 repositories: crate::SharedClaimCheckRegistry,
247 ) {
248 self.claim_check_repositories = repositories;
249 }
250
251 pub(crate) fn set_cache_repositories(&mut self, repositories: crate::SharedCacheRegistry) {
252 self.cache_repositories = repositories;
253 }
254
255 pub fn set_health_registry(&mut self, registry: Arc<HealthCheckRegistry>) {
256 self.health_registry = Some(registry);
257 }
258
259 pub fn set_function_invoker(&mut self, invoker: Arc<dyn FunctionInvoker>) {
260 self.function_invoker = Some(invoker);
261 }
262
263 pub fn set_runtime_handle(&mut self, runtime: Arc<dyn RuntimeHandle>) {
265 self.runtime = Some(Arc::downgrade(&runtime));
266 }
267
268 pub fn set_crash_notifier(&mut self, tx: mpsc::Sender<CrashNotification>) {
273 self.crash_notifier = Some(tx);
274 }
275
276 pub fn set_bind_exposure_acks(&mut self, acks: BindExposureAcks) {
280 self.bind_acks = acks;
281 }
282
283 pub fn set_intercept_rules(&mut self, rules: InterceptRules) -> Result<(), CamelError> {
289 if self.frozen {
290 return Err(CamelError::Config(
291 "intercept rules are frozen: a route was added or the context was started; \
292 rules cannot be changed after first use"
293 .into(),
294 ));
295 }
296 self.intercept = rules;
297 Ok(())
298 }
299
300 pub fn with_intercept_rules(mut self, rules: InterceptRules) -> Self {
303 self.intercept = rules;
304 self
305 }
306
307 pub fn mark_started(&mut self) {
311 self.frozen = true;
312 }
313
314 pub(super) fn plans_for_bind(
319 &self,
320 bind_key: &str,
321 ) -> Vec<(String, camel_api::security_policy::RouteSecurityPlan)> {
322 self.routes
323 .iter()
324 .filter_map(|(route_id, managed)| {
325 let plan = managed.compiled.security_plan.as_ref()?;
326 let bind = bind_key_from_uri(&managed.from_uri)?;
327 if bind.key == bind_key {
328 Some((route_id.clone(), plan.clone()))
329 } else {
330 None
331 }
332 })
333 .collect()
334 }
335
336 fn bind_is_running(&self, bind_key: &str) -> bool {
343 self.routes.iter().any(|(_, managed)| {
344 bind_key_from_uri(&managed.from_uri).is_some_and(|bind| {
345 bind.key == bind_key && handle_is_running(&managed.consumer_handle)
346 })
347 })
348 }
349
350 fn enforce_late_registration_gate(
354 &self,
355 from_uri: &str,
356 route_id: &str,
357 plan: Option<&camel_api::security_policy::RouteSecurityPlan>,
358 ) -> Result<(), CamelError> {
359 let Some(bind) = bind_key_from_uri(from_uri) else {
360 return Ok(());
361 };
362 if !self.bind_is_running(&bind.key) {
363 return Ok(());
364 }
365
366 let mut owned = self.plans_for_bind(&bind.key);
367 if let Some(plan) = plan {
368 owned.push((route_id.to_string(), plan.clone()));
369 }
370 let plans: Vec<(&str, &camel_api::security_policy::RouteSecurityPlan)> =
371 owned.iter().map(|(id, plan)| (id.as_str(), plan)).collect();
372 enforce_bind_exposure_gate(
373 &bind.key,
374 bind.loopback,
375 &plans,
376 self.bind_acks.acknowledged(&bind.key),
377 )
378 .map_err(|err| {
379 CamelError::RouteError(format!(
380 "late registration of route '{route_id}' rejected for bind '{}': {err}",
381 bind.key
382 ))
383 })
384 }
385
386 pub fn set_error_handler(&mut self, config: ErrorHandlerConfig) {
387 self.global_error_handler = Some(config);
388 }
389
390 pub fn set_tracer_config(&mut self, config: &TracerConfig) {
392 self.tracing_enabled = config.enabled;
393 self.tracer_detail_level = config.detail_level.clone();
394 self.tracer_metrics = config.metrics_collector.clone();
395 }
396
397 fn build_producer_context(&self, route_id: &str) -> Result<ProducerContext, CamelError> {
398 let mut producer_ctx = ProducerContext::new().with_route_id(route_id);
399 if let Some(runtime) = self.runtime.as_ref().and_then(Weak::upgrade) {
400 producer_ctx = producer_ctx.with_runtime(runtime);
401 }
402 Ok(producer_ctx)
403 }
404
405 fn route_compiler_ext(&self) -> RouteCompilerExt<'_> {
407 RouteCompilerExt {
408 registry: &self.registry,
409 languages: &self.languages,
410 beans: &self.beans,
411 function_invoker: &self.function_invoker,
412 tracing_enabled: self.tracing_enabled,
413 tracer_detail_level: &self.tracer_detail_level,
414 tracer_metrics: &self.tracer_metrics,
415 platform_service: &self.platform_service,
416 runtime: &self.runtime,
417 global_error_handler: &self.global_error_handler,
418 health_registry: &self.health_registry,
419 route_registry: &self.routes,
420 idempotent_repositories: Arc::clone(&self.idempotent_repositories),
421 claim_check_repositories: Arc::clone(&self.claim_check_repositories),
422 cache_repositories: Arc::clone(&self.cache_repositories),
423 intercept: &self.intercept,
424 }
425 }
426
427 #[allow(dead_code)] pub(crate) fn resolve_steps(
430 &self,
431 steps: Vec<BuilderStep>,
432 producer_ctx: &ProducerContext,
433 registry: &Arc<std::sync::Mutex<Registry>>,
434 route_id: Option<&str>,
435 staging_mode: &super::step_resolution::FunctionStagingMode,
436 ) -> Result<Vec<CompiledStep>, CamelError> {
437 let component_ctx = Arc::new(ControllerComponentContext::new(
438 Arc::clone(registry),
439 Arc::clone(&self.languages),
440 self.tracer_metrics
441 .clone()
442 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
443 Arc::clone(&self.platform_service),
444 self.health_registry(),
445 route_id.map(|s| s.to_string()),
446 ));
447 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
448 Arc::clone(&component_ctx) as Arc<_>;
449
450 super::step_resolution::resolve_steps(
451 steps,
452 producer_ctx,
453 rt,
454 registry,
455 &self.languages,
456 &self.beans,
457 self.function_invoker.clone(),
458 component_ctx,
459 route_id,
460 staging_mode,
461 &self.idempotent_repositories,
462 &self.claim_check_repositories,
463 &self.cache_repositories,
464 self.intercept.clone(),
465 )
466 }
467
468 pub async fn add_route(&mut self, definition: RouteDefinition) -> Result<(), CamelError> {
478 let route_id = definition.route_id().to_string();
479 let from_uri = definition.from_uri().to_string();
480
481 if self.routes.contains_key(&route_id) {
482 return Err(CamelError::RouteError(format!(
483 "duplicate route ID '{route_id}'"
484 )));
485 }
486
487 debug!(route_id = %route_id, "Adding route to controller");
488
489 let managed = match self.build_managed_route(
490 definition,
491 &super::step_resolution::FunctionStagingMode::DirectAdd,
492 ) {
493 Ok(managed) => managed,
494 Err(err) => {
495 self.discard_function_staging();
496 return Err(err);
497 }
498 };
499
500 if let Err(err) = self.enforce_late_registration_gate(
504 &from_uri,
505 &route_id,
506 managed.compiled.security_plan.as_ref(),
507 ) {
508 self.discard_function_staging();
509 return Err(err);
510 }
511
512 if let Some(invoker) = &self.function_invoker
513 && let Err(err) = invoker.commit_staged().await
514 {
515 invoker.discard_staging(0);
516 return Err(CamelError::Config(err.to_string()));
517 }
518
519 self.routes
520 .insert(managed.definition.route_id().to_string(), managed);
521
522 self.endpoint_index.insert(&from_uri, &route_id);
523 self.frozen = true;
526 Ok(())
527 }
528
529 pub(super) fn build_managed_route(
530 &self,
531 definition: RouteDefinition,
532 staging_mode: &super::step_resolution::FunctionStagingMode,
533 ) -> Result<ManagedRoute, CamelError> {
534 let route_id = definition.route_id().to_string();
535
536 let definition_info = definition.to_info();
537
538 let empty_providers;
543 let providers = match &definition.provider_registry {
544 Some(registry) => registry.as_ref(),
545 None => {
546 empty_providers = camel_auth::ProviderRegistry::new();
547 &empty_providers
548 }
549 };
550 let security_plan =
551 super::route_compiler_ext::compile_route_security_plan(&definition, providers)?;
552
553 let RouteDefinition {
554 from_uri,
555 steps,
556 error_handler,
557 circuit_breaker,
558 circuit_breaker_fallback,
559 security_policy,
560 security_authenticator,
561 provider_registry,
562 unit_of_work,
563 concurrency,
564 ..
565 } = definition;
566
567 let producer_ctx = self.build_producer_context(&route_id)?;
568
569 assert_no_mixed_top_level_splits(&steps)?;
571
572 let (aggregate_split, processors_with_contracts) = self
573 .route_compiler_ext()
574 .detect_and_validate_route_split(steps, &producer_ctx, &route_id, staging_mode)?;
575 let mut lifecycle = super::route_helpers::collect_lifecycle(&processors_with_contracts);
576
577 let (circuit_breaker, fallback_lifecycle) = self.route_compiler_ext().attach_cb_fallback(
581 circuit_breaker,
582 circuit_breaker_fallback,
583 &producer_ctx,
584 &route_id,
585 staging_mode,
586 )?;
587 lifecycle.extend(fallback_lifecycle);
588 let route_id_for_tracing = route_id.clone();
589 let eh_config = error_handler.or_else(|| self.global_error_handler.clone());
590 let transport = transport_from_uri(&from_uri);
591
592 let mut pipeline = build_eh_config_pipeline(
593 eh_config.as_ref(),
594 Arc::clone(&self.registry),
595 Arc::clone(&self.languages),
596 self.tracer_metrics.clone(),
597 Arc::clone(&self.platform_service),
598 self.health_registry(),
599 &route_id_for_tracing,
600 &producer_ctx,
601 processors_with_contracts,
602 self.tracing_enabled,
603 self.tracer_detail_level.clone(),
604 security_policy.clone(),
605 transport,
606 circuit_breaker,
607 )?;
608
609 let uow_counter = if let Some(uow_config) = &unit_of_work {
610 let component_ctx = Arc::new(ControllerComponentContext::new(
611 Arc::clone(&self.registry),
612 Arc::clone(&self.languages),
613 self.tracer_metrics
614 .clone()
615 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
616 Arc::clone(&self.platform_service),
617 self.health_registry(),
618 Some(route_id.clone()),
619 ));
620 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
621 Arc::clone(&component_ctx) as Arc<_>;
622 let (uow_layer, counter) = super::route_compiler_ext::resolve_uow_layer(
623 uow_config,
624 &producer_ctx,
625 rt,
626 component_ctx.as_ref(),
627 None,
628 )?;
629 pipeline = BoxProcessor::new(uow_layer.layer(pipeline));
630 Some(counter)
631 } else {
632 None
633 };
634
635 Ok(ManagedRoute {
636 definition: definition_info,
637 from_uri,
638 pipeline: super::pipeline_runtime::new_shared_pipeline_with_lifecycle(
639 pipeline, lifecycle,
640 ),
641 concurrency,
642 consumer_handle: None,
643 pipeline_handle: None,
644 consumer_cancel_token: CancellationToken::new(),
645 pipeline_cancel_token: CancellationToken::new(),
646 channel_sender: None,
647 in_flight: uow_counter,
648 drain_in_flight: Arc::new(std::sync::atomic::AtomicU64::new(0)),
649 aggregate_split,
650 agg_service: None,
651 compiled: route_runtime_state::CompiledRoute {
652 security_policy,
653 security_authenticator,
654 provider_registry,
655 security_plan,
656 },
657 })
658 }
659
660 pub async fn add_route_with_generation(
661 &mut self,
662 definition: RouteDefinition,
663 generation: u64,
664 ) -> Result<(), CamelError> {
665 let route_id = definition.route_id().to_string();
666 let from_uri = definition.from_uri().to_string();
667
668 if self.routes.contains_key(&route_id) {
669 return Err(CamelError::RouteError(format!(
670 "duplicate route ID '{route_id}'"
671 )));
672 }
673
674 debug!(route_id = %route_id, generation, "Adding route to controller with generation");
675
676 let managed = self.build_managed_route(
677 definition,
678 &super::step_resolution::FunctionStagingMode::HotReload { generation },
679 )?;
680
681 if let Err(err) = self.enforce_late_registration_gate(
684 &from_uri,
685 &route_id,
686 managed.compiled.security_plan.as_ref(),
687 ) {
688 self.discard_function_staging();
689 return Err(err);
690 }
691
692 self.routes.insert(route_id.clone(), managed);
693
694 self.endpoint_index.insert(&from_uri, &route_id);
695 Ok(())
696 }
697
698 pub async fn remove_route_preserving_functions(
699 &mut self,
700 route_id: &str,
701 ) -> Result<(), CamelError> {
702 let managed = self.routes.get(route_id).ok_or_else(|| {
703 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
704 })?;
705 if handle_is_running(&managed.consumer_handle)
706 || handle_is_running(&managed.pipeline_handle)
707 {
708 return Err(CamelError::RouteError(format!(
709 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
710 route_id,
711 inferred_lifecycle_label(managed)
712 )));
713 }
714 self.routes.remove(route_id);
715 if let Some(reg) = &self.health_registry {
716 reg.unregister_for_route(route_id);
717 }
718 self.endpoint_index.remove(route_id);
719 debug!(route_id = %route_id, "Route removed from controller (functions preserved for reload finalize)");
720 Ok(())
721 }
722
723 pub fn compile_route_definition(
726 &self,
727 def: RouteDefinition,
728 ) -> Result<BoxProcessor, CamelError> {
729 self.route_compiler_ext().compile_route_definition(def)
730 }
731
732 pub fn compile_route_definition_with_generation(
734 &self,
735 def: RouteDefinition,
736 generation: u64,
737 ) -> Result<BoxProcessor, CamelError> {
738 self.route_compiler_ext()
739 .compile_route_definition_with_generation(def, generation)
740 }
741
742 pub(crate) fn compile_route_definition_pipeline(
747 &self,
748 def: RouteDefinition,
749 generation: u64,
750 ) -> Result<CompiledPipeline, CamelError> {
751 self.route_compiler_ext()
752 .compile_route_definition_pipeline(def, generation)
753 }
754
755 pub(crate) fn compile_route_definition_dry_pipeline(
760 &self,
761 def: RouteDefinition,
762 ) -> Result<CompiledPipeline, CamelError> {
763 self.route_compiler_ext()
764 .compile_route_definition_dry_pipeline(def)
765 }
766
767 pub async fn remove_route(&mut self, route_id: &str) -> Result<(), CamelError> {
773 let managed = self.routes.get(route_id).ok_or_else(|| {
774 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
775 })?;
776 if handle_is_running(&managed.consumer_handle)
777 || handle_is_running(&managed.pipeline_handle)
778 {
779 return Err(CamelError::RouteError(format!(
780 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
781 route_id,
782 inferred_lifecycle_label(managed)
783 )));
784 }
785 if let Some(invoker) = &self.function_invoker {
786 for (id, rid) in self.collect_function_refs(route_id) {
787 if let Err(e) = invoker.unregister(&id, rid.as_deref()).await {
788 warn!(route_id = %route_id, error = %e, "Failed to unregister function during route removal");
789 }
790 }
791 }
792 self.routes.remove(route_id);
793 if let Some(reg) = &self.health_registry {
794 reg.unregister_for_route(route_id);
795 }
796 self.endpoint_index.remove(route_id);
797 info!(route_id = %route_id, "Route removed from controller");
798 Ok(())
799 }
800
801 fn collect_function_refs(
802 &self,
803 route_id: &str,
804 ) -> Vec<(camel_api::FunctionId, Option<String>)> {
805 self.function_invoker
806 .as_ref()
807 .map(|invoker| invoker.function_refs_for_route(route_id))
808 .unwrap_or_default()
809 }
810
811 fn discard_function_staging(&self) {
812 if let Some(invoker) = &self.function_invoker {
813 invoker.discard_staging(0);
814 }
815 }
816
817 pub fn route_count(&self) -> usize {
819 self.routes.route_count()
820 }
821
822 pub fn in_flight_count(&self, route_id: &str) -> Option<u64> {
823 self.routes.in_flight_count(route_id)
824 }
825
826 pub fn route_exists(&self, route_id: &str) -> bool {
828 self.routes.route_exists(route_id)
829 }
830
831 pub fn route_ids(&self) -> Vec<String> {
833 self.routes.route_ids()
834 }
835
836 pub fn route_source_hash(&self, route_id: &str) -> Option<u64> {
837 self.routes.route_source_hash(route_id)
838 }
839
840 pub fn auto_startup_route_ids(&self) -> Vec<String> {
842 self.routes.auto_startup_route_ids()
843 }
844
845 pub fn shutdown_route_ids(&self) -> Vec<String> {
847 self.routes.shutdown_route_ids()
848 }
849
850 pub fn swap_pipeline(
869 &self,
870 route_id: &str,
871 new_pipeline: BoxProcessor,
872 ) -> Result<(), CamelError> {
873 let managed = self
874 .routes
875 .get(route_id)
876 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
877
878 let assembly = managed.pipeline.load();
879 let has_lifecycle = !assembly.lifecycle.is_empty();
880
881 if has_lifecycle || managed.agg_service.is_some() {
882 warn!(
883 route_id = %route_id,
884 "Hot-swap rejected — route has lifecycle/agg steps; use Restart path"
885 );
886 return Err(CamelError::RouteError(format!(
887 "Route '{}' contains stateful steps (lifecycle-bearing). Hot-swap not supported — use restart.",
888 route_id
889 )));
890 }
891
892 drop(assembly);
893
894 if managed.aggregate_split.is_some() {
895 warn!(
896 route_id = %route_id,
897 "swap_pipeline: aggregate routes with timeout do not support hot-reload of pre/post segments"
898 );
899 }
900
901 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, vec![]);
902 debug!(route_id = %route_id, "Pipeline swapped atomically");
903 Ok(())
904 }
905
906 pub(crate) fn swap_pipeline_raw(
916 &self,
917 route_id: &str,
918 new_pipeline: BoxProcessor,
919 lifecycle: Vec<Arc<dyn StepLifecycle>>,
920 ) -> Result<(), CamelError> {
921 let managed = self
922 .routes
923 .get(route_id)
924 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
925 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, lifecycle);
926 debug!(route_id = %route_id, "Pipeline swapped (raw — lifecycle bypass)");
927 Ok(())
928 }
929
930 pub fn route_from_uri(&self, route_id: &str) -> Option<String> {
932 self.routes.route_from_uri(route_id)
933 }
934
935 pub fn routes_for_endpoint(&self, uri: &str) -> Vec<String> {
937 self.endpoint_index.routes_for(uri)
938 }
939
940 pub fn list_endpoint_uris(&self) -> Vec<String> {
942 self.endpoint_index.list_uris()
943 }
944
945 pub fn get_pipeline(&self, route_id: &str) -> Option<BoxProcessor> {
950 self.routes.get_pipeline(route_id)
951 }
952
953 pub(crate) fn route_has_lifecycle(&self, route_id: &str) -> bool {
957 self.routes
958 .get(route_id)
959 .map(|managed| !managed.pipeline.load().lifecycle.is_empty())
960 .unwrap_or(false)
961 }
962
963 pub(super) async fn stop_route_internal(&mut self, route_id: &str) -> Result<(), CamelError> {
965 self.routes.stop_route(route_id).await
966 }
967
968 pub async fn start_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
969 self.start_route(route_id).await
970 }
971
972 pub async fn stop_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
973 self.stop_route(route_id).await
974 }
975}
976
977impl DefaultRouteController {
980 #[allow(clippy::too_many_arguments)]
986 pub(super) async fn start_aggregate_route(
987 &mut self,
988 route_id: &str,
989 split: AggregateSplitInfo,
990 consumer: Box<dyn Consumer>,
991 consumer_ctx: ConsumerContext,
992 mut rx: mpsc::Receiver<ExchangeEnvelope>,
993 crash_notifier: Option<mpsc::Sender<CrashNotification>>,
994 runtime_for_consumer: Option<Weak<dyn RuntimeHandle>>,
995 tx_for_storage: mpsc::Sender<ExchangeEnvelope>,
996 pipeline_cancel: CancellationToken,
998 drain_in_flight: Arc<std::sync::atomic::AtomicU64>,
999 ) -> Result<(), CamelError> {
1000 let (late_tx, late_rx) = mpsc::channel::<Exchange>(256);
1001
1002 let route_cancel_clone = pipeline_cancel.clone();
1003 let svc = AggregatorService::new(
1004 split.agg_config.clone(),
1005 late_tx,
1006 Arc::clone(&self.languages),
1007 route_cancel_clone,
1008 );
1009 let agg = Arc::new(svc);
1010
1011 let pipeline_cancel_for_monitor = pipeline_cancel.clone();
1012 let agg_for_monitor = Arc::clone(&agg);
1013
1014 {
1015 let managed = self
1016 .routes
1017 .get_mut(route_id)
1018 .expect("invariant: route must exist"); managed.agg_service = Some(Arc::clone(&agg));
1020 }
1021
1022 let late_rx = Arc::new(tokio::sync::Mutex::new(late_rx));
1023 let pre_pipeline = split.pre_pipeline;
1024 let post_pipeline = split.post_pipeline;
1025
1026 let pipeline_handle = tokio::spawn(async move {
1028 loop {
1029 tokio::select! {
1030 biased;
1031
1032 late_ex = async {
1033 let mut rx = late_rx.lock().await;
1034 rx.recv().await
1035 } => {
1036 match late_ex {
1037 Some(ex) => {
1038 let pipe = post_pipeline.load();
1039 if let Err(e) = pipe.processor.clone_inner().oneshot(ex).await {
1040 tracing::warn!(error = %e, "late exchange post-pipeline failed");
1041 }
1042 }
1043 None => return,
1044 }
1045 }
1046
1047 envelope_opt = rx.recv() => {
1048 match envelope_opt {
1049 Some(envelope) => {
1050 let ExchangeEnvelope { exchange, reply_tx } = envelope;
1051 let _drain_guard = super::route_helpers::DrainGuard::new(Arc::clone(&drain_in_flight));
1052 let pre_pipe = pre_pipeline.load();
1053 let ex = match pre_pipe.processor.clone_inner().oneshot(exchange).await {
1054 Ok(ex) => ex,
1055 Err(e) => {
1056 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
1057 continue;
1058 }
1059 };
1060
1061 let ex = {
1062 let cloned_svc = agg.as_ref().clone();
1063 cloned_svc.oneshot(ex).await
1064 };
1065
1066 match ex {
1067 Ok(ex) => {
1068 if !is_pending(&ex) {
1069 let post_pipe = post_pipeline.load();
1070 let out = post_pipe.processor.clone_inner().oneshot(ex).await;
1071 if let Some(tx) = reply_tx { let _ = tx.send(out); }
1072 } else if let Some(tx) = reply_tx {
1073 let _ = tx.send(Ok(ex));
1074 }
1075 }
1076 Err(e) => {
1077 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
1078 }
1079 }
1080 }
1081 None => return,
1082 }
1083 }
1084
1085 _ = pipeline_cancel.cancelled() => {
1086 agg.force_complete_all();
1087 let mut rx_guard = late_rx.lock().await;
1088 while let Ok(late_ex) = rx_guard.try_recv() {
1089 let pipe = post_pipeline.load();
1090 let _ = pipe.processor.clone_inner().oneshot(late_ex).await;
1091 }
1092 break;
1093 }
1094 }
1095 }
1096 });
1097 #[cfg(test)]
1098 emit_start_route_event("pipeline_spawned", route_id);
1099
1100 let (consumer_handle, startup_rx) = super::consumer_management::spawn_consumer_task(
1103 route_id.to_string(),
1104 consumer,
1105 consumer_ctx,
1106 crash_notifier,
1107 runtime_for_consumer,
1108 false,
1109 );
1110
1111 if let Err(e) =
1118 super::consumer_management::await_consumer_startup(startup_rx, "startup").await
1119 {
1120 consumer_handle.abort();
1121 pipeline_cancel_for_monitor.cancel();
1122 return Err(e);
1123 }
1124
1125 let force_on_stop = agg_for_monitor.config().force_completion_on_stop;
1129 let consumer_handle = tokio::spawn(async move {
1130 let _ = consumer_handle.await;
1131 if !pipeline_cancel_for_monitor.is_cancelled() {
1132 agg_for_monitor.force_complete_all();
1133 if force_on_stop {
1134 pipeline_cancel_for_monitor.cancel();
1135 }
1136 }
1137 });
1138 #[cfg(test)]
1139 emit_start_route_event("consumer_spawned", route_id);
1140
1141 {
1142 let managed = self
1143 .routes
1144 .get_mut(route_id)
1145 .expect("invariant: route must exist"); managed.consumer_handle = Some(consumer_handle);
1147 managed.pipeline_handle = Some(pipeline_handle);
1148 managed.channel_sender = Some(tx_for_storage);
1149 }
1150
1151 info!(route_id = %route_id, "Route started (aggregate with timeout)");
1152 Ok(())
1153 }
1154
1155 #[cfg(test)]
1160 pub(crate) fn set_route_lifecycle_for_test(
1161 &mut self,
1162 route_id: &str,
1163 lifecycle: Vec<Arc<dyn StepLifecycle>>,
1164 ) -> Result<(), CamelError> {
1165 use super::pipeline_runtime::PipelineAssembly;
1166 use camel_api::SyncBoxProcessor;
1167 use std::sync::Arc;
1168
1169 let managed = self
1170 .routes
1171 .get_mut(route_id)
1172 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
1173 let old_processor = managed.pipeline.load().processor.clone_inner();
1174 managed.pipeline.store(Arc::new(PipelineAssembly::new(
1175 SyncBoxProcessor::new(old_processor),
1176 lifecycle,
1177 )));
1178 Ok(())
1179 }
1180}
1181
1182#[cfg(test)]
1183impl crate::hot_reload::ports::ReloadIntrospectionPort for DefaultRouteController {
1184 fn route_ids(&self) -> Vec<String> {
1185 self.route_ids() }
1187 fn route_from_uri(&self, route_id: &str) -> Option<String> {
1188 self.route_from_uri(route_id) }
1190 fn route_source_hash(&self, route_id: &str) -> Option<u64> {
1191 self.route_source_hash(route_id) }
1193}
1194
1195#[cfg(test)]
1196#[path = "route_controller_tests.rs"]
1197mod tests;