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::CohortActivationGate;
33use crate::lifecycle::adapters::controller_component_context::ControllerComponentContext;
34use crate::lifecycle::adapters::route_compiler_ext::{
35 RouteCompilerExt, build_eh_config_pipeline, transport_from_uri,
36};
37use crate::lifecycle::adapters::route_helpers::{
38 AggregateSplitInfo, CrashNotification, ManagedRoute, assert_no_mixed_top_level_splits,
39 handle_is_running, inferred_lifecycle_label, is_pending,
40};
41#[cfg(test)]
42pub(super) use crate::lifecycle::adapters::route_helpers::{
43 emit_start_route_event, set_start_route_event_hook,
44};
45use crate::lifecycle::adapters::route_registry::RouteRegistry;
46use crate::lifecycle::adapters::route_runtime_state;
47use crate::lifecycle::adapters::step_compilers::CompiledStep;
48use crate::lifecycle::application::route_definition::{BuilderStep, RouteDefinition};
49pub(crate) use crate::lifecycle::domain::CompiledPipeline;
50use crate::shared::components::domain::Registry;
51use crate::shared::observability::domain::{DetailLevel, TracerConfig};
52use camel_bean::BeanRegistry;
53
54pub struct DefaultRouteController {
62 pub(super) routes: RouteRegistry,
64 pub(super) registry: Arc<std::sync::Mutex<Registry>>,
66 pub(super) languages: SharedLanguageRegistry,
68 pub(super) beans: Arc<std::sync::Mutex<BeanRegistry>>,
70 pub(super) runtime: Option<Weak<dyn RuntimeHandle>>,
72 pub(super) global_error_handler: Option<ErrorHandlerConfig>,
74 pub(super) crash_notifier: Option<mpsc::Sender<CrashNotification>>,
76 pub(super) tracing_enabled: bool,
78 pub(super) tracer_detail_level: DetailLevel,
80 pub(super) tracer_metrics: Option<Arc<dyn MetricsCollector>>,
82 pub(super) platform_service: Arc<dyn PlatformService>,
83 pub(super) function_invoker: Option<Arc<dyn FunctionInvoker>>,
84 pub(super) health_registry: Option<Arc<HealthCheckRegistry>>,
85 pub(super) idempotent_repositories: crate::SharedIdempotentRegistry,
89 pub(super) claim_check_repositories: crate::SharedClaimCheckRegistry,
90 pub(super) cache_repositories: crate::SharedCacheRegistry,
91 pub(super) prepared_staging: HashMap<String, ManagedRoute>,
96 pub(super) endpoint_index: super::endpoint_index::EndpointIndex,
98 pub(super) bind_acks: super::route_controller_trait::BindExposureAcks,
101 pub(super) intercept: InterceptRules,
103 pub(super) frozen: bool,
107 pub(super) cohort: Arc<CohortActivationGate>,
111}
112
113impl DefaultRouteController {
114 pub fn activate_cohort(&self) {
123 self.cohort.open();
124 }
125
126 pub(super) fn health_registry(&self) -> Arc<HealthCheckRegistry> {
127 self.health_registry.clone().unwrap_or_else(|| {
128 debug!("health_registry not configured — creating isolated fallback");
129 Arc::new(HealthCheckRegistry::new(Duration::from_secs(5)))
130 })
131 }
132
133 pub fn new(
135 registry: Arc<std::sync::Mutex<Registry>>,
136 platform_service: Arc<dyn PlatformService>,
137 ) -> Self {
138 Self::with_beans_and_platform_service(
139 registry,
140 Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
141 platform_service,
142 )
143 }
144
145 pub fn with_beans(
147 registry: Arc<std::sync::Mutex<Registry>>,
148 beans: Arc<std::sync::Mutex<BeanRegistry>>,
149 ) -> Self {
150 Self::with_beans_and_platform_service(
151 registry,
152 beans,
153 Arc::new(NoopPlatformService::default()),
154 )
155 }
156
157 fn with_beans_and_platform_service(
158 registry: Arc<std::sync::Mutex<Registry>>,
159 beans: Arc<std::sync::Mutex<BeanRegistry>>,
160 platform_service: Arc<dyn PlatformService>,
161 ) -> Self {
162 Self {
163 routes: RouteRegistry::new(),
164 registry,
165 languages: Arc::new(std::sync::Mutex::new(HashMap::new())),
166 beans,
167 runtime: None,
168 global_error_handler: None,
169 crash_notifier: None,
170 tracing_enabled: false,
171 tracer_detail_level: DetailLevel::Minimal,
172 tracer_metrics: None,
173 platform_service,
174 function_invoker: None,
175 health_registry: None,
176 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
177 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
178 cache_repositories: Arc::new(crate::CacheRegistry::new()),
179 prepared_staging: HashMap::new(),
180 endpoint_index: super::endpoint_index::EndpointIndex::new(),
181 bind_acks: Default::default(),
182 intercept: InterceptRules::default(),
183 frozen: false,
184 cohort: Arc::new(CohortActivationGate::new_closed()),
185 }
186 }
187
188 pub fn with_languages(
190 registry: Arc<std::sync::Mutex<Registry>>,
191 languages: SharedLanguageRegistry,
192 platform_service: Arc<dyn PlatformService>,
193 ) -> Self {
194 Self {
195 routes: RouteRegistry::new(),
196 registry,
197 languages,
198 beans: Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
199 runtime: None,
200 global_error_handler: None,
201 crash_notifier: None,
202 tracing_enabled: false,
203 tracer_detail_level: DetailLevel::Minimal,
204 tracer_metrics: None,
205 platform_service,
206 function_invoker: None,
207 health_registry: None,
208 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
209 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
210 cache_repositories: Arc::new(crate::CacheRegistry::new()),
211 prepared_staging: HashMap::new(),
212 endpoint_index: super::endpoint_index::EndpointIndex::new(),
213 bind_acks: Default::default(),
214 intercept: InterceptRules::default(),
215 frozen: false,
216 cohort: Arc::new(CohortActivationGate::new_closed()),
217 }
218 }
219
220 pub fn with_languages_and_beans(
221 registry: Arc<std::sync::Mutex<Registry>>,
222 languages: SharedLanguageRegistry,
223 platform_service: Arc<dyn PlatformService>,
224 beans: Arc<std::sync::Mutex<BeanRegistry>>,
225 ) -> Self {
226 Self {
227 routes: RouteRegistry::new(),
228 registry,
229 languages,
230 beans,
231 runtime: None,
232 global_error_handler: None,
233 crash_notifier: None,
234 tracing_enabled: false,
235 tracer_detail_level: DetailLevel::Minimal,
236 tracer_metrics: None,
237 platform_service,
238 function_invoker: None,
239 health_registry: None,
240 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
241 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
242 cache_repositories: Arc::new(crate::CacheRegistry::new()),
243 prepared_staging: HashMap::new(),
244 endpoint_index: super::endpoint_index::EndpointIndex::new(),
245 bind_acks: Default::default(),
246 intercept: InterceptRules::default(),
247 frozen: false,
248 cohort: Arc::new(CohortActivationGate::new_closed()),
249 }
250 }
251
252 pub fn with_function_invoker(mut self, function_invoker: Arc<dyn FunctionInvoker>) -> Self {
253 self.function_invoker = Some(function_invoker);
254 self
255 }
256
257 pub(crate) fn set_idempotent_repositories(
258 &mut self,
259 repositories: crate::SharedIdempotentRegistry,
260 ) {
261 self.idempotent_repositories = repositories;
262 }
263
264 pub(crate) fn set_claim_check_repositories(
265 &mut self,
266 repositories: crate::SharedClaimCheckRegistry,
267 ) {
268 self.claim_check_repositories = repositories;
269 }
270
271 pub(crate) fn set_cache_repositories(&mut self, repositories: crate::SharedCacheRegistry) {
272 self.cache_repositories = repositories;
273 }
274
275 pub fn set_health_registry(&mut self, registry: Arc<HealthCheckRegistry>) {
276 self.health_registry = Some(registry);
277 }
278
279 pub fn set_function_invoker(&mut self, invoker: Arc<dyn FunctionInvoker>) {
280 self.function_invoker = Some(invoker);
281 }
282
283 pub fn set_runtime_handle(&mut self, runtime: Arc<dyn RuntimeHandle>) {
285 self.runtime = Some(Arc::downgrade(&runtime));
286 }
287
288 pub fn set_crash_notifier(&mut self, tx: mpsc::Sender<CrashNotification>) {
293 self.crash_notifier = Some(tx);
294 }
295
296 pub fn set_bind_exposure_acks(&mut self, acks: BindExposureAcks) {
300 self.bind_acks = acks;
301 }
302
303 pub fn set_intercept_rules(&mut self, rules: InterceptRules) -> Result<(), CamelError> {
309 if self.frozen {
310 return Err(CamelError::Config(
311 "intercept rules are frozen: a route was added or the context was started; \
312 rules cannot be changed after first use"
313 .into(),
314 ));
315 }
316 self.intercept = rules;
317 Ok(())
318 }
319
320 pub fn with_intercept_rules(mut self, rules: InterceptRules) -> Self {
323 self.intercept = rules;
324 self
325 }
326
327 pub fn mark_started(&mut self) {
331 self.frozen = true;
332 }
333
334 pub(super) fn plans_for_bind(
339 &self,
340 bind_key: &str,
341 ) -> Vec<(String, camel_api::security_policy::RouteSecurityPlan)> {
342 self.routes
343 .iter()
344 .filter_map(|(route_id, managed)| {
345 let plan = managed.compiled.security_plan.as_ref()?;
346 let bind = bind_key_from_uri(&managed.from_uri)?;
347 if bind.key == bind_key {
348 Some((route_id.clone(), plan.clone()))
349 } else {
350 None
351 }
352 })
353 .collect()
354 }
355
356 fn bind_is_running(&self, bind_key: &str) -> bool {
363 self.routes.iter().any(|(_, managed)| {
364 bind_key_from_uri(&managed.from_uri).is_some_and(|bind| {
365 bind.key == bind_key && handle_is_running(&managed.consumer_handle)
366 })
367 })
368 }
369
370 fn enforce_late_registration_gate(
374 &self,
375 from_uri: &str,
376 route_id: &str,
377 plan: Option<&camel_api::security_policy::RouteSecurityPlan>,
378 ) -> Result<(), CamelError> {
379 let Some(bind) = bind_key_from_uri(from_uri) else {
380 return Ok(());
381 };
382 if !self.bind_is_running(&bind.key) {
383 return Ok(());
384 }
385
386 let mut owned = self.plans_for_bind(&bind.key);
387 if let Some(plan) = plan {
388 owned.push((route_id.to_string(), plan.clone()));
389 }
390 let plans: Vec<(&str, &camel_api::security_policy::RouteSecurityPlan)> =
391 owned.iter().map(|(id, plan)| (id.as_str(), plan)).collect();
392 enforce_bind_exposure_gate(
393 &bind.key,
394 bind.loopback,
395 &plans,
396 self.bind_acks.acknowledged(&bind.key),
397 )
398 .map_err(|err| {
399 CamelError::RouteError(format!(
400 "late registration of route '{route_id}' rejected for bind '{}': {err}",
401 bind.key
402 ))
403 })
404 }
405
406 pub fn set_error_handler(&mut self, config: ErrorHandlerConfig) {
407 self.global_error_handler = Some(config);
408 }
409
410 pub fn set_tracer_config(&mut self, config: &TracerConfig) {
412 self.tracing_enabled = config.enabled;
413 self.tracer_detail_level = config.detail_level.clone();
414 self.tracer_metrics = config.metrics_collector.clone();
415 }
416
417 fn build_producer_context(&self, route_id: &str) -> Result<ProducerContext, CamelError> {
418 let mut producer_ctx = ProducerContext::new().with_route_id(route_id);
419 if let Some(runtime) = self.runtime.as_ref().and_then(Weak::upgrade) {
420 producer_ctx = producer_ctx.with_runtime(runtime);
421 }
422 Ok(producer_ctx)
423 }
424
425 fn route_compiler_ext(&self) -> RouteCompilerExt<'_> {
427 RouteCompilerExt {
428 registry: &self.registry,
429 languages: &self.languages,
430 beans: &self.beans,
431 function_invoker: &self.function_invoker,
432 tracing_enabled: self.tracing_enabled,
433 tracer_detail_level: &self.tracer_detail_level,
434 tracer_metrics: &self.tracer_metrics,
435 platform_service: &self.platform_service,
436 runtime: &self.runtime,
437 global_error_handler: &self.global_error_handler,
438 health_registry: &self.health_registry,
439 route_registry: &self.routes,
440 idempotent_repositories: Arc::clone(&self.idempotent_repositories),
441 claim_check_repositories: Arc::clone(&self.claim_check_repositories),
442 cache_repositories: Arc::clone(&self.cache_repositories),
443 intercept: &self.intercept,
444 }
445 }
446
447 #[allow(dead_code)] pub(crate) fn resolve_steps(
450 &self,
451 steps: Vec<BuilderStep>,
452 producer_ctx: &ProducerContext,
453 registry: &Arc<std::sync::Mutex<Registry>>,
454 route_id: Option<&str>,
455 staging_mode: &super::step_resolution::FunctionStagingMode,
456 ) -> Result<Vec<CompiledStep>, CamelError> {
457 let component_ctx = Arc::new(ControllerComponentContext::new(
458 Arc::clone(registry),
459 Arc::clone(&self.languages),
460 self.tracer_metrics
461 .clone()
462 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
463 Arc::clone(&self.platform_service),
464 self.health_registry(),
465 route_id.map(|s| s.to_string()),
466 ));
467 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
468 Arc::clone(&component_ctx) as Arc<_>;
469
470 super::step_resolution::resolve_steps(
471 steps,
472 producer_ctx,
473 rt,
474 registry,
475 &self.languages,
476 &self.beans,
477 self.function_invoker.clone(),
478 component_ctx,
479 route_id,
480 staging_mode,
481 &self.idempotent_repositories,
482 &self.claim_check_repositories,
483 &self.cache_repositories,
484 self.intercept.clone(),
485 )
486 }
487
488 pub async fn add_route(&mut self, definition: RouteDefinition) -> Result<(), CamelError> {
498 let route_id = definition.route_id().to_string();
499 let from_uri = definition.from_uri().to_string();
500
501 if self.routes.contains_key(&route_id) {
502 return Err(CamelError::RouteError(format!(
503 "duplicate route ID '{route_id}'"
504 )));
505 }
506
507 debug!(route_id = %route_id, "Adding route to controller");
508
509 let managed = match self.build_managed_route(
510 definition,
511 &super::step_resolution::FunctionStagingMode::DirectAdd,
512 ) {
513 Ok(managed) => managed,
514 Err(err) => {
515 self.discard_function_staging();
516 return Err(err);
517 }
518 };
519
520 if let Err(err) = self.enforce_late_registration_gate(
524 &from_uri,
525 &route_id,
526 managed.compiled.security_plan.as_ref(),
527 ) {
528 self.discard_function_staging();
529 return Err(err);
530 }
531
532 if let Some(invoker) = &self.function_invoker
533 && let Err(err) = invoker.commit_staged().await
534 {
535 invoker.discard_staging(0);
536 return Err(CamelError::Config(err.to_string()));
537 }
538
539 self.routes
540 .insert(managed.definition.route_id().to_string(), managed);
541
542 self.endpoint_index.insert(&from_uri, &route_id);
543 self.frozen = true;
546 Ok(())
547 }
548
549 pub(super) fn build_managed_route(
550 &self,
551 definition: RouteDefinition,
552 staging_mode: &super::step_resolution::FunctionStagingMode,
553 ) -> Result<ManagedRoute, CamelError> {
554 let route_id = definition.route_id().to_string();
555
556 let definition_info = definition.to_info();
557
558 let empty_providers;
563 let providers = match &definition.provider_registry {
564 Some(registry) => registry.as_ref(),
565 None => {
566 empty_providers = camel_auth::ProviderRegistry::new();
567 &empty_providers
568 }
569 };
570 let security_plan =
571 super::route_compiler_ext::compile_route_security_plan(&definition, providers)?;
572
573 let RouteDefinition {
574 from_uri,
575 steps,
576 error_handler,
577 circuit_breaker,
578 circuit_breaker_fallback,
579 security_policy,
580 security_authenticator,
581 provider_registry,
582 unit_of_work,
583 concurrency,
584 ..
585 } = definition;
586
587 let producer_ctx = self.build_producer_context(&route_id)?;
588
589 assert_no_mixed_top_level_splits(&steps)?;
591
592 let (aggregate_split, processors_with_contracts) = self
593 .route_compiler_ext()
594 .detect_and_validate_route_split(steps, &producer_ctx, &route_id, staging_mode)?;
595 let mut lifecycle = super::route_helpers::collect_lifecycle(&processors_with_contracts);
596
597 let (circuit_breaker, fallback_lifecycle) = self.route_compiler_ext().attach_cb_fallback(
601 circuit_breaker,
602 circuit_breaker_fallback,
603 &producer_ctx,
604 &route_id,
605 staging_mode,
606 )?;
607 lifecycle.extend(fallback_lifecycle);
608 let route_id_for_tracing = route_id.clone();
609 let eh_config = error_handler.or_else(|| self.global_error_handler.clone());
610 let transport = transport_from_uri(&from_uri);
611
612 let mut pipeline = build_eh_config_pipeline(
613 eh_config.as_ref(),
614 Arc::clone(&self.registry),
615 Arc::clone(&self.languages),
616 self.tracer_metrics.clone(),
617 Arc::clone(&self.platform_service),
618 self.health_registry(),
619 &route_id_for_tracing,
620 &producer_ctx,
621 processors_with_contracts,
622 self.tracing_enabled,
623 self.tracer_detail_level.clone(),
624 security_policy.clone(),
625 transport,
626 circuit_breaker,
627 )?;
628
629 let uow_counter = if let Some(uow_config) = &unit_of_work {
630 let component_ctx = Arc::new(ControllerComponentContext::new(
631 Arc::clone(&self.registry),
632 Arc::clone(&self.languages),
633 self.tracer_metrics
634 .clone()
635 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
636 Arc::clone(&self.platform_service),
637 self.health_registry(),
638 Some(route_id.clone()),
639 ));
640 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
641 Arc::clone(&component_ctx) as Arc<_>;
642 let (uow_layer, counter) = super::route_compiler_ext::resolve_uow_layer(
643 uow_config,
644 &producer_ctx,
645 rt,
646 component_ctx.as_ref(),
647 None,
648 )?;
649 pipeline = BoxProcessor::new(uow_layer.layer(pipeline));
650 Some(counter)
651 } else {
652 None
653 };
654
655 Ok(ManagedRoute {
656 definition: definition_info,
657 from_uri,
658 pipeline: super::pipeline_runtime::new_shared_pipeline_with_lifecycle(
659 pipeline, lifecycle,
660 ),
661 concurrency,
662 consumer_handle: None,
663 pipeline_handle: None,
664 consumer_cancel_token: CancellationToken::new(),
665 pipeline_cancel_token: CancellationToken::new(),
666 channel_sender: None,
667 in_flight: uow_counter,
668 drain_in_flight: Arc::new(std::sync::atomic::AtomicU64::new(0)),
669 aggregate_split,
670 agg_service: None,
671 compiled: route_runtime_state::CompiledRoute {
672 security_policy,
673 security_authenticator,
674 provider_registry,
675 security_plan,
676 },
677 })
678 }
679
680 pub async fn add_route_with_generation(
681 &mut self,
682 definition: RouteDefinition,
683 generation: u64,
684 ) -> Result<(), CamelError> {
685 let route_id = definition.route_id().to_string();
686 let from_uri = definition.from_uri().to_string();
687
688 if self.routes.contains_key(&route_id) {
689 return Err(CamelError::RouteError(format!(
690 "duplicate route ID '{route_id}'"
691 )));
692 }
693
694 debug!(route_id = %route_id, generation, "Adding route to controller with generation");
695
696 let managed = self.build_managed_route(
697 definition,
698 &super::step_resolution::FunctionStagingMode::HotReload { generation },
699 )?;
700
701 if let Err(err) = self.enforce_late_registration_gate(
704 &from_uri,
705 &route_id,
706 managed.compiled.security_plan.as_ref(),
707 ) {
708 self.discard_function_staging();
709 return Err(err);
710 }
711
712 self.routes.insert(route_id.clone(), managed);
713
714 self.endpoint_index.insert(&from_uri, &route_id);
715 Ok(())
716 }
717
718 pub async fn remove_route_preserving_functions(
719 &mut self,
720 route_id: &str,
721 ) -> Result<(), CamelError> {
722 let managed = self.routes.get(route_id).ok_or_else(|| {
723 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
724 })?;
725 if handle_is_running(&managed.consumer_handle)
726 || handle_is_running(&managed.pipeline_handle)
727 {
728 return Err(CamelError::RouteError(format!(
729 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
730 route_id,
731 inferred_lifecycle_label(managed)
732 )));
733 }
734 self.routes.remove(route_id);
735 if let Some(reg) = &self.health_registry {
736 reg.unregister_for_route(route_id);
737 }
738 self.endpoint_index.remove(route_id);
739 debug!(route_id = %route_id, "Route removed from controller (functions preserved for reload finalize)");
740 Ok(())
741 }
742
743 pub fn compile_route_definition(
746 &self,
747 def: RouteDefinition,
748 ) -> Result<BoxProcessor, CamelError> {
749 self.route_compiler_ext().compile_route_definition(def)
750 }
751
752 pub fn compile_route_definition_with_generation(
754 &self,
755 def: RouteDefinition,
756 generation: u64,
757 ) -> Result<BoxProcessor, CamelError> {
758 self.route_compiler_ext()
759 .compile_route_definition_with_generation(def, generation)
760 }
761
762 pub(crate) fn compile_route_definition_pipeline(
767 &self,
768 def: RouteDefinition,
769 generation: u64,
770 ) -> Result<CompiledPipeline, CamelError> {
771 self.route_compiler_ext()
772 .compile_route_definition_pipeline(def, generation)
773 }
774
775 pub(crate) fn compile_route_definition_dry_pipeline(
780 &self,
781 def: RouteDefinition,
782 ) -> Result<CompiledPipeline, CamelError> {
783 self.route_compiler_ext()
784 .compile_route_definition_dry_pipeline(def)
785 }
786
787 pub async fn remove_route(&mut self, route_id: &str) -> Result<(), CamelError> {
793 let managed = self.routes.get(route_id).ok_or_else(|| {
794 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
795 })?;
796 if handle_is_running(&managed.consumer_handle)
797 || handle_is_running(&managed.pipeline_handle)
798 {
799 return Err(CamelError::RouteError(format!(
800 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
801 route_id,
802 inferred_lifecycle_label(managed)
803 )));
804 }
805 if let Some(invoker) = &self.function_invoker {
806 for (id, rid) in self.collect_function_refs(route_id) {
807 if let Err(e) = invoker.unregister(&id, rid.as_deref()).await {
808 warn!(route_id = %route_id, error = %e, "Failed to unregister function during route removal");
809 }
810 }
811 }
812 self.routes.remove(route_id);
813 if let Some(reg) = &self.health_registry {
814 reg.unregister_for_route(route_id);
815 }
816 self.endpoint_index.remove(route_id);
817 info!(route_id = %route_id, "Route removed from controller");
818 Ok(())
819 }
820
821 fn collect_function_refs(
822 &self,
823 route_id: &str,
824 ) -> Vec<(camel_api::FunctionId, Option<String>)> {
825 self.function_invoker
826 .as_ref()
827 .map(|invoker| invoker.function_refs_for_route(route_id))
828 .unwrap_or_default()
829 }
830
831 fn discard_function_staging(&self) {
832 if let Some(invoker) = &self.function_invoker {
833 invoker.discard_staging(0);
834 }
835 }
836
837 pub fn route_count(&self) -> usize {
839 self.routes.route_count()
840 }
841
842 pub fn in_flight_count(&self, route_id: &str) -> Option<u64> {
843 self.routes.in_flight_count(route_id)
844 }
845
846 pub fn route_exists(&self, route_id: &str) -> bool {
848 self.routes.route_exists(route_id)
849 }
850
851 pub fn route_ids(&self) -> Vec<String> {
853 self.routes.route_ids()
854 }
855
856 pub fn route_source_hash(&self, route_id: &str) -> Option<u64> {
857 self.routes.route_source_hash(route_id)
858 }
859
860 pub fn auto_startup_route_ids(&self) -> Vec<String> {
862 self.routes.auto_startup_route_ids()
863 }
864
865 pub fn shutdown_route_ids(&self) -> Vec<String> {
867 self.routes.shutdown_route_ids()
868 }
869
870 pub fn swap_pipeline(
889 &self,
890 route_id: &str,
891 new_pipeline: BoxProcessor,
892 ) -> Result<(), CamelError> {
893 let managed = self
894 .routes
895 .get(route_id)
896 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
897
898 let assembly = managed.pipeline.load();
899 let has_lifecycle = !assembly.lifecycle.is_empty();
900
901 if has_lifecycle || managed.agg_service.is_some() {
902 warn!(
903 route_id = %route_id,
904 "Hot-swap rejected — route has lifecycle/agg steps; use Restart path"
905 );
906 return Err(CamelError::RouteError(format!(
907 "Route '{}' contains stateful steps (lifecycle-bearing). Hot-swap not supported — use restart.",
908 route_id
909 )));
910 }
911
912 drop(assembly);
913
914 if managed.aggregate_split.is_some() {
915 warn!(
916 route_id = %route_id,
917 "swap_pipeline: aggregate routes with timeout do not support hot-reload of pre/post segments"
918 );
919 }
920
921 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, vec![]);
922 debug!(route_id = %route_id, "Pipeline swapped atomically");
923 Ok(())
924 }
925
926 pub(crate) fn swap_pipeline_raw(
936 &self,
937 route_id: &str,
938 new_pipeline: BoxProcessor,
939 lifecycle: Vec<Arc<dyn StepLifecycle>>,
940 ) -> Result<(), CamelError> {
941 let managed = self
942 .routes
943 .get(route_id)
944 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
945 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, lifecycle);
946 debug!(route_id = %route_id, "Pipeline swapped (raw — lifecycle bypass)");
947 Ok(())
948 }
949
950 pub fn route_from_uri(&self, route_id: &str) -> Option<String> {
952 self.routes.route_from_uri(route_id)
953 }
954
955 pub fn routes_for_endpoint(&self, uri: &str) -> Vec<String> {
957 self.endpoint_index.routes_for(uri)
958 }
959
960 pub fn list_endpoint_uris(&self) -> Vec<String> {
962 self.endpoint_index.list_uris()
963 }
964
965 pub fn get_pipeline(&self, route_id: &str) -> Option<BoxProcessor> {
970 self.routes.get_pipeline(route_id)
971 }
972
973 pub(crate) fn route_has_lifecycle(&self, route_id: &str) -> bool {
977 self.routes
978 .get(route_id)
979 .map(|managed| !managed.pipeline.load().lifecycle.is_empty())
980 .unwrap_or(false)
981 }
982
983 pub(super) async fn stop_route_internal(&mut self, route_id: &str) -> Result<(), CamelError> {
985 self.routes.stop_route(route_id).await
986 }
987
988 pub async fn start_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
989 self.start_route(route_id).await
990 }
991
992 pub async fn stop_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
993 self.stop_route(route_id).await
994 }
995}
996
997impl DefaultRouteController {
1000 #[allow(clippy::too_many_arguments)]
1006 pub(super) async fn start_aggregate_route(
1007 &mut self,
1008 route_id: &str,
1009 split: AggregateSplitInfo,
1010 consumer: Box<dyn Consumer>,
1011 consumer_ctx: ConsumerContext,
1012 mut rx: mpsc::Receiver<ExchangeEnvelope>,
1013 crash_notifier: Option<mpsc::Sender<CrashNotification>>,
1014 runtime_for_consumer: Option<Weak<dyn RuntimeHandle>>,
1015 tx_for_storage: mpsc::Sender<ExchangeEnvelope>,
1016 pipeline_cancel: CancellationToken,
1018 drain_in_flight: Arc<std::sync::atomic::AtomicU64>,
1019 ) -> Result<(), CamelError> {
1020 let (late_tx, late_rx) = mpsc::channel::<Exchange>(256);
1021
1022 let route_cancel_clone = pipeline_cancel.clone();
1023 let svc = AggregatorService::new(
1024 split.agg_config.clone(),
1025 late_tx,
1026 Arc::clone(&self.languages),
1027 route_cancel_clone,
1028 );
1029 let agg = Arc::new(svc);
1030
1031 let pipeline_cancel_for_monitor = pipeline_cancel.clone();
1032 let mut cohort_rx = self.cohort.subscribe();
1035 let agg_for_monitor = Arc::clone(&agg);
1036
1037 {
1038 let managed = self
1039 .routes
1040 .get_mut(route_id)
1041 .expect("invariant: route must exist"); managed.agg_service = Some(Arc::clone(&agg));
1043 }
1044
1045 let late_rx = Arc::new(tokio::sync::Mutex::new(late_rx));
1046 let pre_pipeline = split.pre_pipeline;
1047 let post_pipeline = split.post_pipeline;
1048
1049 let pipeline_handle = tokio::spawn(async move {
1051 loop {
1052 tokio::select! {
1053 biased;
1054
1055 late_ex = async {
1060 let mut rx = late_rx.lock().await;
1061 rx.recv().await
1062 } => {
1063 match late_ex {
1064 Some(ex) => {
1065 let pipe = post_pipeline.load();
1066 if let Err(e) = pipe.processor.clone_inner().oneshot(ex).await {
1067 tracing::warn!(error = %e, "late exchange post-pipeline failed");
1068 }
1069 }
1070 None => return,
1071 }
1072 }
1073
1074 envelope_opt = rx.recv() => {
1075 match envelope_opt {
1076 Some(envelope) => {
1077 tokio::select! {
1081 _ = cohort_rx.wait_for(|open| *open) => {}
1082 _ = pipeline_cancel.cancelled() => {
1083 continue;
1096 }
1097 }
1098 let ExchangeEnvelope { exchange, reply_tx } = envelope;
1099 let _drain_guard = super::route_helpers::DrainGuard::new(Arc::clone(&drain_in_flight));
1100 let pre_pipe = pre_pipeline.load();
1101 let ex = match pre_pipe.processor.clone_inner().oneshot(exchange).await {
1102 Ok(ex) => ex,
1103 Err(e) => {
1104 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
1105 continue;
1106 }
1107 };
1108
1109 let ex = {
1110 let cloned_svc = agg.as_ref().clone();
1111 cloned_svc.oneshot(ex).await
1112 };
1113
1114 match ex {
1115 Ok(ex) => {
1116 if !is_pending(&ex) {
1117 let post_pipe = post_pipeline.load();
1118 let out = post_pipe.processor.clone_inner().oneshot(ex).await;
1119 if let Some(tx) = reply_tx { let _ = tx.send(out); }
1120 } else if let Some(tx) = reply_tx {
1121 let _ = tx.send(Ok(ex));
1122 }
1123 }
1124 Err(e) => {
1125 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
1126 }
1127 }
1128 }
1129 None => return,
1130 }
1131 }
1132
1133 _ = pipeline_cancel.cancelled() => {
1134 agg.force_complete_all();
1135 let mut rx_guard = late_rx.lock().await;
1136 while let Ok(late_ex) = rx_guard.try_recv() {
1137 let pipe = post_pipeline.load();
1138 let _ = pipe.processor.clone_inner().oneshot(late_ex).await;
1139 }
1140 break;
1141 }
1142 }
1143 }
1144 });
1145 #[cfg(test)]
1146 emit_start_route_event("pipeline_spawned", route_id);
1147
1148 let consumer_cancel_for_cleanup = consumer_ctx.cancel_token();
1154 let (consumer_handle, startup_rx, watcher_inputs, outer_inputs) =
1155 super::consumer_management::spawn_consumer_task(
1156 route_id.to_string(),
1157 consumer,
1158 consumer_ctx,
1159 crash_notifier,
1160 runtime_for_consumer,
1161 false,
1162 );
1163
1164 let startup_result =
1168 super::consumer_management::await_consumer_startup(startup_rx, "startup").await;
1169 if let Err(e) = startup_result {
1174 consumer_handle.abort();
1175 pipeline_cancel_for_monitor.cancel();
1176 consumer_cancel_for_cleanup.cancel();
1178 return Err(e);
1179 }
1180
1181 if let Some(inputs) = watcher_inputs {
1183 super::consumer_management::spawn_failure_watcher(inputs);
1184 }
1185
1186 if let Some(outer) = outer_inputs {
1192 super::consumer_management::spawn_outer_task_watcher(outer);
1193 }
1194
1195 let force_on_stop = agg_for_monitor.config().force_completion_on_stop;
1199 let consumer_handle = tokio::spawn(async move {
1200 let _ = consumer_handle.await;
1201 if !pipeline_cancel_for_monitor.is_cancelled() {
1202 agg_for_monitor.force_complete_all();
1203 if force_on_stop {
1204 pipeline_cancel_for_monitor.cancel();
1205 }
1206 }
1207 });
1208 #[cfg(test)]
1209 emit_start_route_event("consumer_spawned", route_id);
1210
1211 {
1212 let managed = self
1213 .routes
1214 .get_mut(route_id)
1215 .expect("invariant: route must exist"); managed.consumer_handle = Some(consumer_handle);
1217 managed.pipeline_handle = Some(pipeline_handle);
1218 managed.channel_sender = Some(tx_for_storage);
1219 }
1220
1221 info!(route_id = %route_id, "Route started (aggregate with timeout)");
1222 Ok(())
1223 }
1224
1225 #[cfg(test)]
1230 pub(crate) fn set_route_lifecycle_for_test(
1231 &mut self,
1232 route_id: &str,
1233 lifecycle: Vec<Arc<dyn StepLifecycle>>,
1234 ) -> Result<(), CamelError> {
1235 use super::pipeline_runtime::PipelineAssembly;
1236 use camel_api::SyncBoxProcessor;
1237 use std::sync::Arc;
1238
1239 let managed = self
1240 .routes
1241 .get_mut(route_id)
1242 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
1243 let old_processor = managed.pipeline.load().processor.clone_inner();
1244 managed.pipeline.store(Arc::new(PipelineAssembly::new(
1245 SyncBoxProcessor::new(old_processor),
1246 lifecycle,
1247 )));
1248 Ok(())
1249 }
1250}
1251
1252#[cfg(test)]
1253impl crate::hot_reload::ports::ReloadIntrospectionPort for DefaultRouteController {
1254 fn route_ids(&self) -> Vec<String> {
1255 self.route_ids() }
1257 fn route_from_uri(&self, route_id: &str) -> Option<String> {
1258 self.route_from_uri(route_id) }
1260 fn route_source_hash(&self, route_id: &str) -> Option<u64> {
1261 self.route_source_hash(route_id) }
1263}
1264
1265#[cfg(test)]
1266#[path = "route_controller_tests.rs"]
1267mod tests;
1268
1269#[cfg(test)]
1270#[path = "cohort_activation_regression.rs"]
1271mod cohort_activation_regression;