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::lifecycle::adapters::controller_component_context::ControllerComponentContext;
32use crate::lifecycle::adapters::route_compiler_ext::{
33 RouteCompilerExt, build_eh_config_pipeline, transport_from_uri,
34};
35use crate::lifecycle::adapters::route_helpers::{
36 AggregateSplitInfo, CrashNotification, ManagedRoute, assert_no_mixed_top_level_splits,
37 handle_is_running, inferred_lifecycle_label, is_pending,
38};
39#[cfg(test)]
40pub(super) use crate::lifecycle::adapters::route_helpers::{
41 emit_start_route_event, set_start_route_event_hook,
42};
43use crate::lifecycle::adapters::route_registry::RouteRegistry;
44use crate::lifecycle::adapters::route_runtime_state;
45use crate::lifecycle::adapters::step_compilers::CompiledStep;
46use crate::lifecycle::application::route_definition::{BuilderStep, RouteDefinition};
47pub(crate) use crate::lifecycle::domain::CompiledPipeline;
48use crate::shared::components::domain::Registry;
49use crate::shared::observability::domain::{DetailLevel, TracerConfig};
50use camel_bean::BeanRegistry;
51
52pub struct DefaultRouteController {
60 pub(super) routes: RouteRegistry,
62 pub(super) registry: Arc<std::sync::Mutex<Registry>>,
64 pub(super) languages: SharedLanguageRegistry,
66 pub(super) beans: Arc<std::sync::Mutex<BeanRegistry>>,
68 pub(super) runtime: Option<Weak<dyn RuntimeHandle>>,
70 pub(super) global_error_handler: Option<ErrorHandlerConfig>,
72 pub(super) crash_notifier: Option<mpsc::Sender<CrashNotification>>,
74 pub(super) tracing_enabled: bool,
76 pub(super) tracer_detail_level: DetailLevel,
78 pub(super) tracer_metrics: Option<Arc<dyn MetricsCollector>>,
80 pub(super) platform_service: Arc<dyn PlatformService>,
81 pub(super) function_invoker: Option<Arc<dyn FunctionInvoker>>,
82 pub(super) health_registry: Option<Arc<HealthCheckRegistry>>,
83 pub(super) idempotent_repositories: crate::SharedIdempotentRegistry,
87 pub(super) claim_check_repositories: crate::SharedClaimCheckRegistry,
88 pub(super) cache_repositories: crate::SharedCacheRegistry,
89 pub(super) prepared_staging: HashMap<String, ManagedRoute>,
94 pub(super) endpoint_index: super::endpoint_index::EndpointIndex,
96 pub(super) bind_acks: super::route_controller_trait::BindExposureAcks,
99}
100
101impl DefaultRouteController {
102 pub(super) fn health_registry(&self) -> Arc<HealthCheckRegistry> {
103 self.health_registry.clone().unwrap_or_else(|| {
104 debug!("health_registry not configured — creating isolated fallback");
105 Arc::new(HealthCheckRegistry::new(Duration::from_secs(5)))
106 })
107 }
108
109 pub fn new(
111 registry: Arc<std::sync::Mutex<Registry>>,
112 platform_service: Arc<dyn PlatformService>,
113 ) -> Self {
114 Self::with_beans_and_platform_service(
115 registry,
116 Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
117 platform_service,
118 )
119 }
120
121 pub fn with_beans(
123 registry: Arc<std::sync::Mutex<Registry>>,
124 beans: Arc<std::sync::Mutex<BeanRegistry>>,
125 ) -> Self {
126 Self::with_beans_and_platform_service(
127 registry,
128 beans,
129 Arc::new(NoopPlatformService::default()),
130 )
131 }
132
133 fn with_beans_and_platform_service(
134 registry: Arc<std::sync::Mutex<Registry>>,
135 beans: Arc<std::sync::Mutex<BeanRegistry>>,
136 platform_service: Arc<dyn PlatformService>,
137 ) -> Self {
138 Self {
139 routes: RouteRegistry::new(),
140 registry,
141 languages: Arc::new(std::sync::Mutex::new(HashMap::new())),
142 beans,
143 runtime: None,
144 global_error_handler: None,
145 crash_notifier: None,
146 tracing_enabled: false,
147 tracer_detail_level: DetailLevel::Minimal,
148 tracer_metrics: None,
149 platform_service,
150 function_invoker: None,
151 health_registry: None,
152 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
153 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
154 cache_repositories: Arc::new(crate::CacheRegistry::new()),
155 prepared_staging: HashMap::new(),
156 endpoint_index: super::endpoint_index::EndpointIndex::new(),
157 bind_acks: Default::default(),
158 }
159 }
160
161 pub fn with_languages(
163 registry: Arc<std::sync::Mutex<Registry>>,
164 languages: SharedLanguageRegistry,
165 platform_service: Arc<dyn PlatformService>,
166 ) -> Self {
167 Self {
168 routes: RouteRegistry::new(),
169 registry,
170 languages,
171 beans: Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
172 runtime: None,
173 global_error_handler: None,
174 crash_notifier: None,
175 tracing_enabled: false,
176 tracer_detail_level: DetailLevel::Minimal,
177 tracer_metrics: None,
178 platform_service,
179 function_invoker: None,
180 health_registry: None,
181 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
182 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
183 cache_repositories: Arc::new(crate::CacheRegistry::new()),
184 prepared_staging: HashMap::new(),
185 endpoint_index: super::endpoint_index::EndpointIndex::new(),
186 bind_acks: Default::default(),
187 }
188 }
189
190 pub fn with_languages_and_beans(
191 registry: Arc<std::sync::Mutex<Registry>>,
192 languages: SharedLanguageRegistry,
193 platform_service: Arc<dyn PlatformService>,
194 beans: Arc<std::sync::Mutex<BeanRegistry>>,
195 ) -> Self {
196 Self {
197 routes: RouteRegistry::new(),
198 registry,
199 languages,
200 beans,
201 runtime: None,
202 global_error_handler: None,
203 crash_notifier: None,
204 tracing_enabled: false,
205 tracer_detail_level: DetailLevel::Minimal,
206 tracer_metrics: None,
207 platform_service,
208 function_invoker: None,
209 health_registry: None,
210 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
211 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
212 cache_repositories: Arc::new(crate::CacheRegistry::new()),
213 prepared_staging: HashMap::new(),
214 endpoint_index: super::endpoint_index::EndpointIndex::new(),
215 bind_acks: Default::default(),
216 }
217 }
218
219 pub fn with_function_invoker(mut self, function_invoker: Arc<dyn FunctionInvoker>) -> Self {
220 self.function_invoker = Some(function_invoker);
221 self
222 }
223
224 pub(crate) fn set_idempotent_repositories(
225 &mut self,
226 repositories: crate::SharedIdempotentRegistry,
227 ) {
228 self.idempotent_repositories = repositories;
229 }
230
231 pub(crate) fn set_claim_check_repositories(
232 &mut self,
233 repositories: crate::SharedClaimCheckRegistry,
234 ) {
235 self.claim_check_repositories = repositories;
236 }
237
238 pub(crate) fn set_cache_repositories(&mut self, repositories: crate::SharedCacheRegistry) {
239 self.cache_repositories = repositories;
240 }
241
242 pub fn set_health_registry(&mut self, registry: Arc<HealthCheckRegistry>) {
243 self.health_registry = Some(registry);
244 }
245
246 pub fn set_function_invoker(&mut self, invoker: Arc<dyn FunctionInvoker>) {
247 self.function_invoker = Some(invoker);
248 }
249
250 pub fn set_runtime_handle(&mut self, runtime: Arc<dyn RuntimeHandle>) {
252 self.runtime = Some(Arc::downgrade(&runtime));
253 }
254
255 pub fn set_crash_notifier(&mut self, tx: mpsc::Sender<CrashNotification>) {
260 self.crash_notifier = Some(tx);
261 }
262
263 pub fn set_bind_exposure_acks(&mut self, acks: BindExposureAcks) {
267 self.bind_acks = acks;
268 }
269
270 pub(super) fn plans_for_bind(
275 &self,
276 bind_key: &str,
277 ) -> Vec<(String, camel_api::security_policy::RouteSecurityPlan)> {
278 self.routes
279 .iter()
280 .filter_map(|(route_id, managed)| {
281 let plan = managed.compiled.security_plan.as_ref()?;
282 let bind = bind_key_from_uri(&managed.from_uri)?;
283 if bind.key == bind_key {
284 Some((route_id.clone(), plan.clone()))
285 } else {
286 None
287 }
288 })
289 .collect()
290 }
291
292 fn bind_is_running(&self, bind_key: &str) -> bool {
299 self.routes.iter().any(|(_, managed)| {
300 bind_key_from_uri(&managed.from_uri).is_some_and(|bind| {
301 bind.key == bind_key && handle_is_running(&managed.consumer_handle)
302 })
303 })
304 }
305
306 fn enforce_late_registration_gate(
310 &self,
311 from_uri: &str,
312 route_id: &str,
313 plan: Option<&camel_api::security_policy::RouteSecurityPlan>,
314 ) -> Result<(), CamelError> {
315 let Some(bind) = bind_key_from_uri(from_uri) else {
316 return Ok(());
317 };
318 if !self.bind_is_running(&bind.key) {
319 return Ok(());
320 }
321
322 let mut owned = self.plans_for_bind(&bind.key);
323 if let Some(plan) = plan {
324 owned.push((route_id.to_string(), plan.clone()));
325 }
326 let plans: Vec<(&str, &camel_api::security_policy::RouteSecurityPlan)> =
327 owned.iter().map(|(id, plan)| (id.as_str(), plan)).collect();
328 enforce_bind_exposure_gate(
329 &bind.key,
330 bind.loopback,
331 &plans,
332 self.bind_acks.acknowledged(&bind.key),
333 )
334 .map_err(|err| {
335 CamelError::RouteError(format!(
336 "late registration of route '{route_id}' rejected for bind '{}': {err}",
337 bind.key
338 ))
339 })
340 }
341
342 pub fn set_error_handler(&mut self, config: ErrorHandlerConfig) {
343 self.global_error_handler = Some(config);
344 }
345
346 pub fn set_tracer_config(&mut self, config: &TracerConfig) {
348 self.tracing_enabled = config.enabled;
349 self.tracer_detail_level = config.detail_level.clone();
350 self.tracer_metrics = config.metrics_collector.clone();
351 }
352
353 fn build_producer_context(&self, route_id: &str) -> Result<ProducerContext, CamelError> {
354 let mut producer_ctx = ProducerContext::new().with_route_id(route_id);
355 if let Some(runtime) = self.runtime.as_ref().and_then(Weak::upgrade) {
356 producer_ctx = producer_ctx.with_runtime(runtime);
357 }
358 Ok(producer_ctx)
359 }
360
361 fn route_compiler_ext(&self) -> RouteCompilerExt<'_> {
363 RouteCompilerExt {
364 registry: &self.registry,
365 languages: &self.languages,
366 beans: &self.beans,
367 function_invoker: &self.function_invoker,
368 tracing_enabled: self.tracing_enabled,
369 tracer_detail_level: &self.tracer_detail_level,
370 tracer_metrics: &self.tracer_metrics,
371 platform_service: &self.platform_service,
372 runtime: &self.runtime,
373 global_error_handler: &self.global_error_handler,
374 health_registry: &self.health_registry,
375 route_registry: &self.routes,
376 idempotent_repositories: Arc::clone(&self.idempotent_repositories),
377 claim_check_repositories: Arc::clone(&self.claim_check_repositories),
378 cache_repositories: Arc::clone(&self.cache_repositories),
379 }
380 }
381
382 #[allow(dead_code)] pub(crate) fn resolve_steps(
385 &self,
386 steps: Vec<BuilderStep>,
387 producer_ctx: &ProducerContext,
388 registry: &Arc<std::sync::Mutex<Registry>>,
389 route_id: Option<&str>,
390 staging_mode: &super::step_resolution::FunctionStagingMode,
391 ) -> Result<Vec<CompiledStep>, CamelError> {
392 let component_ctx = Arc::new(ControllerComponentContext::new(
393 Arc::clone(registry),
394 Arc::clone(&self.languages),
395 self.tracer_metrics
396 .clone()
397 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
398 Arc::clone(&self.platform_service),
399 self.health_registry(),
400 route_id.map(|s| s.to_string()),
401 ));
402 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
403 Arc::clone(&component_ctx) as Arc<_>;
404
405 super::step_resolution::resolve_steps(
406 steps,
407 producer_ctx,
408 rt,
409 registry,
410 &self.languages,
411 &self.beans,
412 self.function_invoker.clone(),
413 component_ctx,
414 route_id,
415 staging_mode,
416 &self.idempotent_repositories,
417 &self.claim_check_repositories,
418 &self.cache_repositories,
419 )
420 }
421
422 pub async fn add_route(&mut self, definition: RouteDefinition) -> Result<(), CamelError> {
432 let route_id = definition.route_id().to_string();
433 let from_uri = definition.from_uri().to_string();
434
435 if self.routes.contains_key(&route_id) {
436 return Err(CamelError::RouteError(format!(
437 "duplicate route ID '{route_id}'"
438 )));
439 }
440
441 debug!(route_id = %route_id, "Adding route to controller");
442
443 let managed = match self.build_managed_route(
444 definition,
445 &super::step_resolution::FunctionStagingMode::DirectAdd,
446 ) {
447 Ok(managed) => managed,
448 Err(err) => {
449 self.discard_function_staging();
450 return Err(err);
451 }
452 };
453
454 if let Err(err) = self.enforce_late_registration_gate(
458 &from_uri,
459 &route_id,
460 managed.compiled.security_plan.as_ref(),
461 ) {
462 self.discard_function_staging();
463 return Err(err);
464 }
465
466 if let Some(invoker) = &self.function_invoker
467 && let Err(err) = invoker.commit_staged().await
468 {
469 invoker.discard_staging(0);
470 return Err(CamelError::Config(err.to_string()));
471 }
472
473 self.routes
474 .insert(managed.definition.route_id().to_string(), managed);
475
476 self.endpoint_index.insert(&from_uri, &route_id);
477 Ok(())
478 }
479
480 pub(super) fn build_managed_route(
481 &self,
482 definition: RouteDefinition,
483 staging_mode: &super::step_resolution::FunctionStagingMode,
484 ) -> Result<ManagedRoute, CamelError> {
485 let route_id = definition.route_id().to_string();
486
487 let definition_info = definition.to_info();
488
489 let empty_providers;
494 let providers = match &definition.provider_registry {
495 Some(registry) => registry.as_ref(),
496 None => {
497 empty_providers = camel_auth::ProviderRegistry::new();
498 &empty_providers
499 }
500 };
501 let security_plan =
502 super::route_compiler_ext::compile_route_security_plan(&definition, providers)?;
503
504 let RouteDefinition {
505 from_uri,
506 steps,
507 error_handler,
508 circuit_breaker,
509 circuit_breaker_fallback,
510 security_policy,
511 security_authenticator,
512 provider_registry,
513 unit_of_work,
514 concurrency,
515 ..
516 } = definition;
517
518 let producer_ctx = self.build_producer_context(&route_id)?;
519
520 assert_no_mixed_top_level_splits(&steps)?;
522
523 let (aggregate_split, processors_with_contracts) = self
524 .route_compiler_ext()
525 .detect_and_validate_route_split(steps, &producer_ctx, &route_id, staging_mode)?;
526 let mut lifecycle = super::route_helpers::collect_lifecycle(&processors_with_contracts);
527
528 let (circuit_breaker, fallback_lifecycle) = self.route_compiler_ext().attach_cb_fallback(
532 circuit_breaker,
533 circuit_breaker_fallback,
534 &producer_ctx,
535 &route_id,
536 staging_mode,
537 )?;
538 lifecycle.extend(fallback_lifecycle);
539 let route_id_for_tracing = route_id.clone();
540 let eh_config = error_handler.or_else(|| self.global_error_handler.clone());
541 let transport = transport_from_uri(&from_uri);
542
543 let mut pipeline = build_eh_config_pipeline(
544 eh_config.as_ref(),
545 Arc::clone(&self.registry),
546 Arc::clone(&self.languages),
547 self.tracer_metrics.clone(),
548 Arc::clone(&self.platform_service),
549 self.health_registry(),
550 &route_id_for_tracing,
551 &producer_ctx,
552 processors_with_contracts,
553 self.tracing_enabled,
554 self.tracer_detail_level.clone(),
555 security_policy.clone(),
556 transport,
557 circuit_breaker,
558 )?;
559
560 let uow_counter = if let Some(uow_config) = &unit_of_work {
561 let component_ctx = Arc::new(ControllerComponentContext::new(
562 Arc::clone(&self.registry),
563 Arc::clone(&self.languages),
564 self.tracer_metrics
565 .clone()
566 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
567 Arc::clone(&self.platform_service),
568 self.health_registry(),
569 Some(route_id.clone()),
570 ));
571 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
572 Arc::clone(&component_ctx) as Arc<_>;
573 let (uow_layer, counter) = super::route_compiler_ext::resolve_uow_layer(
574 uow_config,
575 &producer_ctx,
576 rt,
577 component_ctx.as_ref(),
578 None,
579 )?;
580 pipeline = BoxProcessor::new(uow_layer.layer(pipeline));
581 Some(counter)
582 } else {
583 None
584 };
585
586 Ok(ManagedRoute {
587 definition: definition_info,
588 from_uri,
589 pipeline: super::pipeline_runtime::new_shared_pipeline_with_lifecycle(
590 pipeline, lifecycle,
591 ),
592 concurrency,
593 consumer_handle: None,
594 pipeline_handle: None,
595 consumer_cancel_token: CancellationToken::new(),
596 pipeline_cancel_token: CancellationToken::new(),
597 channel_sender: None,
598 in_flight: uow_counter,
599 drain_in_flight: Arc::new(std::sync::atomic::AtomicU64::new(0)),
600 aggregate_split,
601 agg_service: None,
602 compiled: route_runtime_state::CompiledRoute {
603 security_policy,
604 security_authenticator,
605 provider_registry,
606 security_plan,
607 },
608 })
609 }
610
611 pub async fn add_route_with_generation(
612 &mut self,
613 definition: RouteDefinition,
614 generation: u64,
615 ) -> Result<(), CamelError> {
616 let route_id = definition.route_id().to_string();
617 let from_uri = definition.from_uri().to_string();
618
619 if self.routes.contains_key(&route_id) {
620 return Err(CamelError::RouteError(format!(
621 "duplicate route ID '{route_id}'"
622 )));
623 }
624
625 debug!(route_id = %route_id, generation, "Adding route to controller with generation");
626
627 let managed = self.build_managed_route(
628 definition,
629 &super::step_resolution::FunctionStagingMode::HotReload { generation },
630 )?;
631
632 if let Err(err) = self.enforce_late_registration_gate(
635 &from_uri,
636 &route_id,
637 managed.compiled.security_plan.as_ref(),
638 ) {
639 self.discard_function_staging();
640 return Err(err);
641 }
642
643 self.routes.insert(route_id.clone(), managed);
644
645 self.endpoint_index.insert(&from_uri, &route_id);
646 Ok(())
647 }
648
649 pub async fn remove_route_preserving_functions(
650 &mut self,
651 route_id: &str,
652 ) -> Result<(), CamelError> {
653 let managed = self.routes.get(route_id).ok_or_else(|| {
654 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
655 })?;
656 if handle_is_running(&managed.consumer_handle)
657 || handle_is_running(&managed.pipeline_handle)
658 {
659 return Err(CamelError::RouteError(format!(
660 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
661 route_id,
662 inferred_lifecycle_label(managed)
663 )));
664 }
665 self.routes.remove(route_id);
666 if let Some(reg) = &self.health_registry {
667 reg.unregister_for_route(route_id);
668 }
669 self.endpoint_index.remove(route_id);
670 debug!(route_id = %route_id, "Route removed from controller (functions preserved for reload finalize)");
671 Ok(())
672 }
673
674 pub fn compile_route_definition(
677 &self,
678 def: RouteDefinition,
679 ) -> Result<BoxProcessor, CamelError> {
680 self.route_compiler_ext().compile_route_definition(def)
681 }
682
683 pub fn compile_route_definition_with_generation(
685 &self,
686 def: RouteDefinition,
687 generation: u64,
688 ) -> Result<BoxProcessor, CamelError> {
689 self.route_compiler_ext()
690 .compile_route_definition_with_generation(def, generation)
691 }
692
693 pub(crate) fn compile_route_definition_pipeline(
698 &self,
699 def: RouteDefinition,
700 generation: u64,
701 ) -> Result<CompiledPipeline, CamelError> {
702 self.route_compiler_ext()
703 .compile_route_definition_pipeline(def, generation)
704 }
705
706 pub(crate) fn compile_route_definition_dry_pipeline(
711 &self,
712 def: RouteDefinition,
713 ) -> Result<CompiledPipeline, CamelError> {
714 self.route_compiler_ext()
715 .compile_route_definition_dry_pipeline(def)
716 }
717
718 pub async fn remove_route(&mut self, route_id: &str) -> Result<(), CamelError> {
724 let managed = self.routes.get(route_id).ok_or_else(|| {
725 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
726 })?;
727 if handle_is_running(&managed.consumer_handle)
728 || handle_is_running(&managed.pipeline_handle)
729 {
730 return Err(CamelError::RouteError(format!(
731 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
732 route_id,
733 inferred_lifecycle_label(managed)
734 )));
735 }
736 if let Some(invoker) = &self.function_invoker {
737 for (id, rid) in self.collect_function_refs(route_id) {
738 if let Err(e) = invoker.unregister(&id, rid.as_deref()).await {
739 warn!(route_id = %route_id, error = %e, "Failed to unregister function during route removal");
740 }
741 }
742 }
743 self.routes.remove(route_id);
744 if let Some(reg) = &self.health_registry {
745 reg.unregister_for_route(route_id);
746 }
747 self.endpoint_index.remove(route_id);
748 info!(route_id = %route_id, "Route removed from controller");
749 Ok(())
750 }
751
752 fn collect_function_refs(
753 &self,
754 route_id: &str,
755 ) -> Vec<(camel_api::FunctionId, Option<String>)> {
756 self.function_invoker
757 .as_ref()
758 .map(|invoker| invoker.function_refs_for_route(route_id))
759 .unwrap_or_default()
760 }
761
762 fn discard_function_staging(&self) {
763 if let Some(invoker) = &self.function_invoker {
764 invoker.discard_staging(0);
765 }
766 }
767
768 pub fn route_count(&self) -> usize {
770 self.routes.route_count()
771 }
772
773 pub fn in_flight_count(&self, route_id: &str) -> Option<u64> {
774 self.routes.in_flight_count(route_id)
775 }
776
777 pub fn route_exists(&self, route_id: &str) -> bool {
779 self.routes.route_exists(route_id)
780 }
781
782 pub fn route_ids(&self) -> Vec<String> {
784 self.routes.route_ids()
785 }
786
787 pub fn route_source_hash(&self, route_id: &str) -> Option<u64> {
788 self.routes.route_source_hash(route_id)
789 }
790
791 pub fn auto_startup_route_ids(&self) -> Vec<String> {
793 self.routes.auto_startup_route_ids()
794 }
795
796 pub fn shutdown_route_ids(&self) -> Vec<String> {
798 self.routes.shutdown_route_ids()
799 }
800
801 pub fn swap_pipeline(
820 &self,
821 route_id: &str,
822 new_pipeline: BoxProcessor,
823 ) -> Result<(), CamelError> {
824 let managed = self
825 .routes
826 .get(route_id)
827 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
828
829 let assembly = managed.pipeline.load();
830 let has_lifecycle = !assembly.lifecycle.is_empty();
831
832 if has_lifecycle || managed.agg_service.is_some() {
833 warn!(
834 route_id = %route_id,
835 "Hot-swap rejected — route has lifecycle/agg steps; use Restart path"
836 );
837 return Err(CamelError::RouteError(format!(
838 "Route '{}' contains stateful steps (lifecycle-bearing). Hot-swap not supported — use restart.",
839 route_id
840 )));
841 }
842
843 drop(assembly);
844
845 if managed.aggregate_split.is_some() {
846 warn!(
847 route_id = %route_id,
848 "swap_pipeline: aggregate routes with timeout do not support hot-reload of pre/post segments"
849 );
850 }
851
852 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, vec![]);
853 debug!(route_id = %route_id, "Pipeline swapped atomically");
854 Ok(())
855 }
856
857 pub(crate) fn swap_pipeline_raw(
867 &self,
868 route_id: &str,
869 new_pipeline: BoxProcessor,
870 lifecycle: Vec<Arc<dyn StepLifecycle>>,
871 ) -> Result<(), CamelError> {
872 let managed = self
873 .routes
874 .get(route_id)
875 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
876 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, lifecycle);
877 debug!(route_id = %route_id, "Pipeline swapped (raw — lifecycle bypass)");
878 Ok(())
879 }
880
881 pub fn route_from_uri(&self, route_id: &str) -> Option<String> {
883 self.routes.route_from_uri(route_id)
884 }
885
886 pub fn routes_for_endpoint(&self, uri: &str) -> Vec<String> {
888 self.endpoint_index.routes_for(uri)
889 }
890
891 pub fn list_endpoint_uris(&self) -> Vec<String> {
893 self.endpoint_index.list_uris()
894 }
895
896 pub fn get_pipeline(&self, route_id: &str) -> Option<BoxProcessor> {
901 self.routes.get_pipeline(route_id)
902 }
903
904 pub(crate) fn route_has_lifecycle(&self, route_id: &str) -> bool {
908 self.routes
909 .get(route_id)
910 .map(|managed| !managed.pipeline.load().lifecycle.is_empty())
911 .unwrap_or(false)
912 }
913
914 pub(super) async fn stop_route_internal(&mut self, route_id: &str) -> Result<(), CamelError> {
916 self.routes.stop_route(route_id).await
917 }
918
919 pub async fn start_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
920 self.start_route(route_id).await
921 }
922
923 pub async fn stop_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
924 self.stop_route(route_id).await
925 }
926}
927
928impl DefaultRouteController {
931 #[allow(clippy::too_many_arguments)]
937 pub(super) async fn start_aggregate_route(
938 &mut self,
939 route_id: &str,
940 split: AggregateSplitInfo,
941 consumer: Box<dyn Consumer>,
942 consumer_ctx: ConsumerContext,
943 mut rx: mpsc::Receiver<ExchangeEnvelope>,
944 crash_notifier: Option<mpsc::Sender<CrashNotification>>,
945 runtime_for_consumer: Option<Weak<dyn RuntimeHandle>>,
946 tx_for_storage: mpsc::Sender<ExchangeEnvelope>,
947 pipeline_cancel: CancellationToken,
949 drain_in_flight: Arc<std::sync::atomic::AtomicU64>,
950 ) -> Result<(), CamelError> {
951 let (late_tx, late_rx) = mpsc::channel::<Exchange>(256);
952
953 let route_cancel_clone = pipeline_cancel.clone();
954 let svc = AggregatorService::new(
955 split.agg_config.clone(),
956 late_tx,
957 Arc::clone(&self.languages),
958 route_cancel_clone,
959 );
960 let agg = Arc::new(svc);
961
962 let pipeline_cancel_for_monitor = pipeline_cancel.clone();
963 let agg_for_monitor = Arc::clone(&agg);
964
965 {
966 let managed = self
967 .routes
968 .get_mut(route_id)
969 .expect("invariant: route must exist"); managed.agg_service = Some(Arc::clone(&agg));
971 }
972
973 let late_rx = Arc::new(tokio::sync::Mutex::new(late_rx));
974 let pre_pipeline = split.pre_pipeline;
975 let post_pipeline = split.post_pipeline;
976
977 let pipeline_handle = tokio::spawn(async move {
979 loop {
980 tokio::select! {
981 biased;
982
983 late_ex = async {
984 let mut rx = late_rx.lock().await;
985 rx.recv().await
986 } => {
987 match late_ex {
988 Some(ex) => {
989 let pipe = post_pipeline.load();
990 if let Err(e) = pipe.processor.clone_inner().oneshot(ex).await {
991 tracing::warn!(error = %e, "late exchange post-pipeline failed");
992 }
993 }
994 None => return,
995 }
996 }
997
998 envelope_opt = rx.recv() => {
999 match envelope_opt {
1000 Some(envelope) => {
1001 let ExchangeEnvelope { exchange, reply_tx } = envelope;
1002 let _drain_guard = super::route_helpers::DrainGuard::new(Arc::clone(&drain_in_flight));
1003 let pre_pipe = pre_pipeline.load();
1004 let ex = match pre_pipe.processor.clone_inner().oneshot(exchange).await {
1005 Ok(ex) => ex,
1006 Err(e) => {
1007 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
1008 continue;
1009 }
1010 };
1011
1012 let ex = {
1013 let cloned_svc = agg.as_ref().clone();
1014 cloned_svc.oneshot(ex).await
1015 };
1016
1017 match ex {
1018 Ok(ex) => {
1019 if !is_pending(&ex) {
1020 let post_pipe = post_pipeline.load();
1021 let out = post_pipe.processor.clone_inner().oneshot(ex).await;
1022 if let Some(tx) = reply_tx { let _ = tx.send(out); }
1023 } else if let Some(tx) = reply_tx {
1024 let _ = tx.send(Ok(ex));
1025 }
1026 }
1027 Err(e) => {
1028 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
1029 }
1030 }
1031 }
1032 None => return,
1033 }
1034 }
1035
1036 _ = pipeline_cancel.cancelled() => {
1037 agg.force_complete_all();
1038 let mut rx_guard = late_rx.lock().await;
1039 while let Ok(late_ex) = rx_guard.try_recv() {
1040 let pipe = post_pipeline.load();
1041 let _ = pipe.processor.clone_inner().oneshot(late_ex).await;
1042 }
1043 break;
1044 }
1045 }
1046 }
1047 });
1048 #[cfg(test)]
1049 emit_start_route_event("pipeline_spawned");
1050
1051 let (consumer_handle, startup_rx) = super::consumer_management::spawn_consumer_task(
1054 route_id.to_string(),
1055 consumer,
1056 consumer_ctx,
1057 crash_notifier,
1058 runtime_for_consumer,
1059 false,
1060 );
1061
1062 if let Err(e) =
1069 super::consumer_management::await_consumer_startup(startup_rx, "startup").await
1070 {
1071 consumer_handle.abort();
1072 pipeline_cancel_for_monitor.cancel();
1073 return Err(e);
1074 }
1075
1076 let force_on_stop = agg_for_monitor.config().force_completion_on_stop;
1080 let consumer_handle = tokio::spawn(async move {
1081 let _ = consumer_handle.await;
1082 if !pipeline_cancel_for_monitor.is_cancelled() {
1083 agg_for_monitor.force_complete_all();
1084 if force_on_stop {
1085 pipeline_cancel_for_monitor.cancel();
1086 }
1087 }
1088 });
1089 #[cfg(test)]
1090 emit_start_route_event("consumer_spawned");
1091
1092 {
1093 let managed = self
1094 .routes
1095 .get_mut(route_id)
1096 .expect("invariant: route must exist"); managed.consumer_handle = Some(consumer_handle);
1098 managed.pipeline_handle = Some(pipeline_handle);
1099 managed.channel_sender = Some(tx_for_storage);
1100 }
1101
1102 info!(route_id = %route_id, "Route started (aggregate with timeout)");
1103 Ok(())
1104 }
1105
1106 #[cfg(test)]
1111 pub(crate) fn set_route_lifecycle_for_test(
1112 &mut self,
1113 route_id: &str,
1114 lifecycle: Vec<Arc<dyn StepLifecycle>>,
1115 ) -> Result<(), CamelError> {
1116 use super::pipeline_runtime::PipelineAssembly;
1117 use camel_api::SyncBoxProcessor;
1118 use std::sync::Arc;
1119
1120 let managed = self
1121 .routes
1122 .get_mut(route_id)
1123 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
1124 let old_processor = managed.pipeline.load().processor.clone_inner();
1125 managed.pipeline.store(Arc::new(PipelineAssembly::new(
1126 SyncBoxProcessor::new(old_processor),
1127 lifecycle,
1128 )));
1129 Ok(())
1130 }
1131}
1132
1133#[cfg(test)]
1134impl crate::hot_reload::ports::ReloadIntrospectionPort for DefaultRouteController {
1135 fn route_ids(&self) -> Vec<String> {
1136 self.route_ids() }
1138 fn route_from_uri(&self, route_id: &str) -> Option<String> {
1139 self.route_from_uri(route_id) }
1141 fn route_source_hash(&self, route_id: &str) -> Option<u64> {
1142 self.route_source_hash(route_id) }
1144}
1145
1146#[cfg(test)]
1147#[path = "route_controller_tests.rs"]
1148mod tests;