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::TracerPipelineGating;
35use crate::lifecycle::adapters::route_compiler_ext::{
36 RouteCompilerExt, build_eh_config_pipeline, transport_from_uri,
37};
38use crate::lifecycle::adapters::route_helpers::{
39 AggregateSplitInfo, CrashNotification, ManagedRoute, assert_no_mixed_top_level_splits,
40 handle_is_running, inferred_lifecycle_label, is_pending,
41};
42#[cfg(test)]
43pub(super) use crate::lifecycle::adapters::route_helpers::{
44 emit_start_route_event, set_start_route_event_hook,
45};
46use crate::lifecycle::adapters::route_registry::RouteRegistry;
47use crate::lifecycle::adapters::route_runtime_state;
48use crate::lifecycle::adapters::step_compilers::CompiledStep;
49use crate::lifecycle::application::route_definition::{BuilderStep, RouteDefinition};
50pub(crate) use crate::lifecycle::domain::CompiledPipeline;
51use crate::shared::components::domain::Registry;
52use crate::shared::observability::domain::{DetailLevel, TracerConfig};
53use camel_bean::BeanRegistry;
54
55pub struct DefaultRouteController {
63 pub(super) routes: RouteRegistry,
65 pub(super) registry: Arc<std::sync::Mutex<Registry>>,
67 pub(super) languages: SharedLanguageRegistry,
69 pub(super) beans: Arc<std::sync::Mutex<BeanRegistry>>,
71 pub(super) runtime: Option<Weak<dyn RuntimeHandle>>,
73 pub(super) global_error_handler: Option<ErrorHandlerConfig>,
75 pub(super) crash_notifier: Option<mpsc::Sender<CrashNotification>>,
77 pub(super) tracer_gating: TracerPipelineGating,
79 pub(super) tracer_detail_level: DetailLevel,
81 pub(super) tracer_metrics: Option<Arc<dyn MetricsCollector>>,
83 pub(super) platform_service: Arc<dyn PlatformService>,
84 pub(super) function_invoker: Option<Arc<dyn FunctionInvoker>>,
85 pub(super) health_registry: Option<Arc<HealthCheckRegistry>>,
86 pub(super) idempotent_repositories: crate::SharedIdempotentRegistry,
90 pub(super) claim_check_repositories: crate::SharedClaimCheckRegistry,
91 pub(super) cache_repositories: crate::SharedCacheRegistry,
92 pub(super) prepared_staging: HashMap<String, ManagedRoute>,
97 pub(super) endpoint_index: super::endpoint_index::EndpointIndex,
99 pub(super) bind_acks: super::route_controller_trait::BindExposureAcks,
102 pub(super) intercept: InterceptRules,
104 pub(super) frozen: bool,
108 pub(super) cohort: Arc<CohortActivationGate>,
112}
113
114impl DefaultRouteController {
115 pub fn activate_cohort(&self) {
124 self.cohort.open();
125 }
126
127 pub(super) fn health_registry(&self) -> Arc<HealthCheckRegistry> {
128 self.health_registry.clone().unwrap_or_else(|| {
129 debug!("health_registry not configured — creating isolated fallback");
130 Arc::new(HealthCheckRegistry::new(Duration::from_secs(5)))
131 })
132 }
133
134 pub fn new(
136 registry: Arc<std::sync::Mutex<Registry>>,
137 platform_service: Arc<dyn PlatformService>,
138 ) -> Self {
139 Self::with_beans_and_platform_service(
140 registry,
141 Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
142 platform_service,
143 )
144 }
145
146 pub fn with_beans(
148 registry: Arc<std::sync::Mutex<Registry>>,
149 beans: Arc<std::sync::Mutex<BeanRegistry>>,
150 ) -> Self {
151 Self::with_beans_and_platform_service(
152 registry,
153 beans,
154 Arc::new(NoopPlatformService::default()),
155 )
156 }
157
158 fn with_beans_and_platform_service(
159 registry: Arc<std::sync::Mutex<Registry>>,
160 beans: Arc<std::sync::Mutex<BeanRegistry>>,
161 platform_service: Arc<dyn PlatformService>,
162 ) -> Self {
163 Self {
164 routes: RouteRegistry::new(),
165 registry,
166 languages: Arc::new(std::sync::Mutex::new(HashMap::new())),
167 beans,
168 runtime: None,
169 global_error_handler: None,
170 crash_notifier: None,
171 tracer_gating: TracerPipelineGating::off(),
172 tracer_detail_level: DetailLevel::Minimal,
173 tracer_metrics: None,
174 platform_service,
175 function_invoker: None,
176 health_registry: None,
177 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
178 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
179 cache_repositories: Arc::new(crate::CacheRegistry::new()),
180 prepared_staging: HashMap::new(),
181 endpoint_index: super::endpoint_index::EndpointIndex::new(),
182 bind_acks: Default::default(),
183 intercept: InterceptRules::default(),
184 frozen: false,
185 cohort: Arc::new(CohortActivationGate::new_closed()),
186 }
187 }
188
189 pub fn with_languages(
191 registry: Arc<std::sync::Mutex<Registry>>,
192 languages: SharedLanguageRegistry,
193 platform_service: Arc<dyn PlatformService>,
194 ) -> Self {
195 Self {
196 routes: RouteRegistry::new(),
197 registry,
198 languages,
199 beans: Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
200 runtime: None,
201 global_error_handler: None,
202 crash_notifier: None,
203 tracer_gating: TracerPipelineGating::off(),
204 tracer_detail_level: DetailLevel::Minimal,
205 tracer_metrics: None,
206 platform_service,
207 function_invoker: None,
208 health_registry: None,
209 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
210 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
211 cache_repositories: Arc::new(crate::CacheRegistry::new()),
212 prepared_staging: HashMap::new(),
213 endpoint_index: super::endpoint_index::EndpointIndex::new(),
214 bind_acks: Default::default(),
215 intercept: InterceptRules::default(),
216 frozen: false,
217 cohort: Arc::new(CohortActivationGate::new_closed()),
218 }
219 }
220
221 pub fn with_languages_and_beans(
222 registry: Arc<std::sync::Mutex<Registry>>,
223 languages: SharedLanguageRegistry,
224 platform_service: Arc<dyn PlatformService>,
225 beans: Arc<std::sync::Mutex<BeanRegistry>>,
226 ) -> Self {
227 Self {
228 routes: RouteRegistry::new(),
229 registry,
230 languages,
231 beans,
232 runtime: None,
233 global_error_handler: None,
234 crash_notifier: None,
235 tracer_gating: TracerPipelineGating::off(),
236 tracer_detail_level: DetailLevel::Minimal,
237 tracer_metrics: None,
238 platform_service,
239 function_invoker: None,
240 health_registry: None,
241 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
242 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
243 cache_repositories: Arc::new(crate::CacheRegistry::new()),
244 prepared_staging: HashMap::new(),
245 endpoint_index: super::endpoint_index::EndpointIndex::new(),
246 bind_acks: Default::default(),
247 intercept: InterceptRules::default(),
248 frozen: false,
249 cohort: Arc::new(CohortActivationGate::new_closed()),
250 }
251 }
252
253 pub fn with_function_invoker(mut self, function_invoker: Arc<dyn FunctionInvoker>) -> Self {
254 self.function_invoker = Some(function_invoker);
255 self
256 }
257
258 pub(crate) fn set_idempotent_repositories(
259 &mut self,
260 repositories: crate::SharedIdempotentRegistry,
261 ) {
262 self.idempotent_repositories = repositories;
263 }
264
265 pub(crate) fn set_claim_check_repositories(
266 &mut self,
267 repositories: crate::SharedClaimCheckRegistry,
268 ) {
269 self.claim_check_repositories = repositories;
270 }
271
272 pub(crate) fn set_cache_repositories(&mut self, repositories: crate::SharedCacheRegistry) {
273 self.cache_repositories = repositories;
274 }
275
276 pub fn set_health_registry(&mut self, registry: Arc<HealthCheckRegistry>) {
277 self.health_registry = Some(registry);
278 }
279
280 pub fn set_function_invoker(&mut self, invoker: Arc<dyn FunctionInvoker>) {
281 self.function_invoker = Some(invoker);
282 }
283
284 pub fn set_runtime_handle(&mut self, runtime: Arc<dyn RuntimeHandle>) {
286 self.runtime = Some(Arc::downgrade(&runtime));
287 }
288
289 pub fn set_crash_notifier(&mut self, tx: mpsc::Sender<CrashNotification>) {
294 self.crash_notifier = Some(tx);
295 }
296
297 pub fn set_bind_exposure_acks(&mut self, acks: BindExposureAcks) {
301 self.bind_acks = acks;
302 }
303
304 pub fn set_intercept_rules(&mut self, rules: InterceptRules) -> Result<(), CamelError> {
310 if self.frozen {
311 return Err(CamelError::Config(
312 "intercept rules are frozen: a route was added or the context was started; \
313 rules cannot be changed after first use"
314 .into(),
315 ));
316 }
317 self.intercept = rules;
318 Ok(())
319 }
320
321 pub fn with_intercept_rules(mut self, rules: InterceptRules) -> Self {
324 self.intercept = rules;
325 self
326 }
327
328 pub fn mark_started(&mut self) {
332 self.frozen = true;
333 }
334
335 pub(super) fn plans_for_bind(
340 &self,
341 bind_key: &str,
342 ) -> Vec<(String, camel_api::security_policy::RouteSecurityPlan)> {
343 self.routes
344 .iter()
345 .filter_map(|(route_id, managed)| {
346 let plan = managed.compiled.security_plan.as_ref()?;
347 let bind = bind_key_from_uri(&managed.from_uri)?;
348 if bind.key == bind_key {
349 Some((route_id.clone(), plan.clone()))
350 } else {
351 None
352 }
353 })
354 .collect()
355 }
356
357 fn bind_is_running(&self, bind_key: &str) -> bool {
364 self.routes.iter().any(|(_, managed)| {
365 bind_key_from_uri(&managed.from_uri).is_some_and(|bind| {
366 bind.key == bind_key && handle_is_running(&managed.consumer_handle)
367 })
368 })
369 }
370
371 fn enforce_late_registration_gate(
375 &self,
376 from_uri: &str,
377 route_id: &str,
378 plan: Option<&camel_api::security_policy::RouteSecurityPlan>,
379 ) -> Result<(), CamelError> {
380 let Some(bind) = bind_key_from_uri(from_uri) else {
381 return Ok(());
382 };
383 if !self.bind_is_running(&bind.key) {
384 return Ok(());
385 }
386
387 let mut owned = self.plans_for_bind(&bind.key);
388 if let Some(plan) = plan {
389 owned.push((route_id.to_string(), plan.clone()));
390 }
391 let plans: Vec<(&str, &camel_api::security_policy::RouteSecurityPlan)> =
392 owned.iter().map(|(id, plan)| (id.as_str(), plan)).collect();
393 enforce_bind_exposure_gate(
394 &bind.key,
395 bind.loopback,
396 &plans,
397 self.bind_acks.acknowledged(&bind.key),
398 )
399 .map_err(|err| {
400 CamelError::RouteError(format!(
401 "late registration of route '{route_id}' rejected for bind '{}': {err}",
402 bind.key
403 ))
404 })
405 }
406
407 pub fn set_error_handler(&mut self, config: ErrorHandlerConfig) {
408 self.global_error_handler = Some(config);
409 }
410
411 pub fn set_tracer_config(&mut self, config: &TracerConfig) {
413 self.tracer_gating = TracerPipelineGating {
416 pipeline_enabled: config.enabled || config.pipeline_enabled,
417 spans_enabled: config.enabled,
418 levers: config.metrics_levers.clone(),
419 };
420 self.tracer_detail_level = config.detail_level.clone();
421 }
422
423 pub fn set_tracer_metrics(&mut self, metrics: Arc<dyn MetricsCollector>) {
430 self.tracer_metrics = Some(metrics);
431 }
432
433 fn build_producer_context(&self, route_id: &str) -> Result<ProducerContext, CamelError> {
434 let mut producer_ctx = ProducerContext::new().with_route_id(route_id);
435 if let Some(runtime) = self.runtime.as_ref().and_then(Weak::upgrade) {
436 producer_ctx = producer_ctx.with_runtime(runtime);
437 }
438 Ok(producer_ctx)
439 }
440
441 fn route_compiler_ext(&self) -> RouteCompilerExt<'_> {
443 RouteCompilerExt {
444 registry: &self.registry,
445 languages: &self.languages,
446 beans: &self.beans,
447 function_invoker: &self.function_invoker,
448 tracer_gating: self.tracer_gating.clone(),
449 tracer_detail_level: &self.tracer_detail_level,
450 tracer_metrics: &self.tracer_metrics,
451 platform_service: &self.platform_service,
452 runtime: &self.runtime,
453 global_error_handler: &self.global_error_handler,
454 health_registry: &self.health_registry,
455 route_registry: &self.routes,
456 idempotent_repositories: Arc::clone(&self.idempotent_repositories),
457 claim_check_repositories: Arc::clone(&self.claim_check_repositories),
458 cache_repositories: Arc::clone(&self.cache_repositories),
459 intercept: &self.intercept,
460 }
461 }
462
463 #[allow(dead_code)] pub(crate) fn resolve_steps(
466 &self,
467 steps: Vec<BuilderStep>,
468 producer_ctx: &ProducerContext,
469 registry: &Arc<std::sync::Mutex<Registry>>,
470 route_id: Option<&str>,
471 staging_mode: &super::step_resolution::FunctionStagingMode,
472 ) -> Result<Vec<CompiledStep>, CamelError> {
473 let component_ctx = Arc::new(ControllerComponentContext::new(
474 Arc::clone(registry),
475 Arc::clone(&self.languages),
476 self.tracer_metrics
477 .clone()
478 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
479 Arc::clone(&self.platform_service),
480 self.health_registry(),
481 route_id.map(|s| s.to_string()),
482 self.tracer_gating.levers.components_enabled(),
483 ));
484 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
485 Arc::clone(&component_ctx) as Arc<_>;
486
487 super::step_resolution::resolve_steps(
488 steps,
489 producer_ctx,
490 rt,
491 registry,
492 &self.languages,
493 &self.beans,
494 self.function_invoker.clone(),
495 component_ctx,
496 route_id,
497 staging_mode,
498 &self.idempotent_repositories,
499 &self.claim_check_repositories,
500 &self.cache_repositories,
501 self.intercept.clone(),
502 )
503 }
504
505 pub async fn add_route(&mut self, definition: RouteDefinition) -> Result<(), CamelError> {
515 let route_id = definition.route_id().to_string();
516 let from_uri = definition.from_uri().to_string();
517
518 if self.routes.contains_key(&route_id) {
519 return Err(CamelError::RouteError(format!(
520 "duplicate route ID '{route_id}'"
521 )));
522 }
523
524 debug!(route_id = %route_id, "Adding route to controller");
525
526 let managed = match self.build_managed_route(
527 definition,
528 &super::step_resolution::FunctionStagingMode::DirectAdd,
529 ) {
530 Ok(managed) => managed,
531 Err(err) => {
532 self.discard_function_staging();
533 return Err(err);
534 }
535 };
536
537 if let Err(err) = self.enforce_late_registration_gate(
541 &from_uri,
542 &route_id,
543 managed.compiled.security_plan.as_ref(),
544 ) {
545 self.discard_function_staging();
546 return Err(err);
547 }
548
549 if let Some(invoker) = &self.function_invoker
550 && let Err(err) = invoker.commit_staged().await
551 {
552 invoker.discard_staging(0);
553 return Err(CamelError::Config(err.to_string()));
554 }
555
556 self.routes
557 .insert(managed.definition.route_id().to_string(), managed);
558
559 self.endpoint_index.insert(&from_uri, &route_id);
560 self.frozen = true;
563 Ok(())
564 }
565
566 pub(super) fn build_managed_route(
567 &self,
568 definition: RouteDefinition,
569 staging_mode: &super::step_resolution::FunctionStagingMode,
570 ) -> Result<ManagedRoute, CamelError> {
571 let route_id = definition.route_id().to_string();
572
573 let definition_info = definition.to_info();
574
575 let empty_providers;
580 let providers = match &definition.provider_registry {
581 Some(registry) => registry.as_ref(),
582 None => {
583 empty_providers = camel_auth::ProviderRegistry::new();
584 &empty_providers
585 }
586 };
587 let security_plan =
588 super::route_compiler_ext::compile_route_security_plan(&definition, providers)?;
589
590 let RouteDefinition {
591 from_uri,
592 steps,
593 error_handler,
594 circuit_breaker,
595 circuit_breaker_fallback,
596 security_policy,
597 security_authenticator,
598 provider_registry,
599 unit_of_work,
600 concurrency,
601 ..
602 } = definition;
603
604 let producer_ctx = self.build_producer_context(&route_id)?;
605
606 assert_no_mixed_top_level_splits(&steps)?;
608
609 let (aggregate_split, processors_with_contracts) = self
610 .route_compiler_ext()
611 .detect_and_validate_route_split(steps, &producer_ctx, &route_id, staging_mode)?;
612 let mut lifecycle = super::route_helpers::collect_lifecycle(&processors_with_contracts);
613
614 let (circuit_breaker, fallback_lifecycle) = self.route_compiler_ext().attach_cb_fallback(
618 circuit_breaker,
619 circuit_breaker_fallback,
620 &producer_ctx,
621 &route_id,
622 staging_mode,
623 )?;
624 lifecycle.extend(fallback_lifecycle);
625 let route_id_for_tracing = route_id.clone();
626 let eh_config = error_handler.or_else(|| self.global_error_handler.clone());
627 let transport = transport_from_uri(&from_uri);
628
629 let mut pipeline = build_eh_config_pipeline(
630 eh_config.as_ref(),
631 Arc::clone(&self.registry),
632 Arc::clone(&self.languages),
633 self.tracer_metrics.clone(),
634 Arc::clone(&self.platform_service),
635 self.health_registry(),
636 &route_id_for_tracing,
637 &producer_ctx,
638 processors_with_contracts,
639 self.tracer_gating.clone(),
640 self.tracer_detail_level.clone(),
641 security_policy.clone(),
642 transport,
643 circuit_breaker,
644 )?;
645
646 let uow_counter = if let Some(uow_config) = &unit_of_work {
647 let component_ctx = Arc::new(ControllerComponentContext::new(
648 Arc::clone(&self.registry),
649 Arc::clone(&self.languages),
650 self.tracer_metrics
651 .clone()
652 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
653 Arc::clone(&self.platform_service),
654 self.health_registry(),
655 Some(route_id.clone()),
656 self.tracer_gating.levers.components_enabled(),
657 ));
658 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
659 Arc::clone(&component_ctx) as Arc<_>;
660 let (uow_layer, counter) = super::route_compiler_ext::resolve_uow_layer(
661 uow_config,
662 &producer_ctx,
663 rt,
664 component_ctx.as_ref(),
665 None,
666 )?;
667 pipeline = BoxProcessor::new(uow_layer.layer(pipeline));
668 Some(counter)
669 } else {
670 None
671 };
672
673 Ok(ManagedRoute {
674 definition: definition_info,
675 from_uri,
676 pipeline: super::pipeline_runtime::new_shared_pipeline_with_lifecycle(
677 pipeline, lifecycle,
678 ),
679 concurrency,
680 consumer_handle: None,
681 pipeline_handle: None,
682 consumer_cancel_token: CancellationToken::new(),
683 pipeline_cancel_token: CancellationToken::new(),
684 channel_sender: None,
685 in_flight: uow_counter,
686 drain_in_flight: Arc::new(std::sync::atomic::AtomicU64::new(0)),
687 aggregate_split,
688 agg_service: None,
689 compiled: route_runtime_state::CompiledRoute {
690 security_policy,
691 security_authenticator,
692 provider_registry,
693 security_plan,
694 },
695 })
696 }
697
698 pub async fn add_route_with_generation(
699 &mut self,
700 definition: RouteDefinition,
701 generation: u64,
702 ) -> Result<(), CamelError> {
703 let route_id = definition.route_id().to_string();
704 let from_uri = definition.from_uri().to_string();
705
706 if self.routes.contains_key(&route_id) {
707 return Err(CamelError::RouteError(format!(
708 "duplicate route ID '{route_id}'"
709 )));
710 }
711
712 debug!(route_id = %route_id, generation, "Adding route to controller with generation");
713
714 let managed = self.build_managed_route(
715 definition,
716 &super::step_resolution::FunctionStagingMode::HotReload { generation },
717 )?;
718
719 if let Err(err) = self.enforce_late_registration_gate(
722 &from_uri,
723 &route_id,
724 managed.compiled.security_plan.as_ref(),
725 ) {
726 self.discard_function_staging();
727 return Err(err);
728 }
729
730 self.routes.insert(route_id.clone(), managed);
731
732 self.endpoint_index.insert(&from_uri, &route_id);
733 Ok(())
734 }
735
736 pub async fn remove_route_preserving_functions(
737 &mut self,
738 route_id: &str,
739 ) -> Result<(), CamelError> {
740 let managed = self.routes.get(route_id).ok_or_else(|| {
741 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
742 })?;
743 if handle_is_running(&managed.consumer_handle)
744 || handle_is_running(&managed.pipeline_handle)
745 {
746 return Err(CamelError::RouteError(format!(
747 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
748 route_id,
749 inferred_lifecycle_label(managed)
750 )));
751 }
752 self.routes.remove(route_id);
753 if let Some(reg) = &self.health_registry {
754 reg.unregister_for_route(route_id);
755 }
756 self.endpoint_index.remove(route_id);
757 debug!(route_id = %route_id, "Route removed from controller (functions preserved for reload finalize)");
758 Ok(())
759 }
760
761 pub fn compile_route_definition(
764 &self,
765 def: RouteDefinition,
766 ) -> Result<BoxProcessor, CamelError> {
767 self.route_compiler_ext().compile_route_definition(def)
768 }
769
770 pub fn compile_route_definition_with_generation(
772 &self,
773 def: RouteDefinition,
774 generation: u64,
775 ) -> Result<BoxProcessor, CamelError> {
776 self.route_compiler_ext()
777 .compile_route_definition_with_generation(def, generation)
778 }
779
780 pub(crate) fn compile_route_definition_pipeline(
785 &self,
786 def: RouteDefinition,
787 generation: u64,
788 ) -> Result<CompiledPipeline, CamelError> {
789 self.route_compiler_ext()
790 .compile_route_definition_pipeline(def, generation)
791 }
792
793 pub(crate) fn compile_route_definition_dry_pipeline(
798 &self,
799 def: RouteDefinition,
800 ) -> Result<CompiledPipeline, CamelError> {
801 self.route_compiler_ext()
802 .compile_route_definition_dry_pipeline(def)
803 }
804
805 pub async fn remove_route(&mut self, route_id: &str) -> Result<(), CamelError> {
811 let managed = self.routes.get(route_id).ok_or_else(|| {
812 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
813 })?;
814 if handle_is_running(&managed.consumer_handle)
815 || handle_is_running(&managed.pipeline_handle)
816 {
817 return Err(CamelError::RouteError(format!(
818 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
819 route_id,
820 inferred_lifecycle_label(managed)
821 )));
822 }
823 if let Some(invoker) = &self.function_invoker {
824 for (id, rid) in self.collect_function_refs(route_id) {
825 if let Err(e) = invoker.unregister(&id, rid.as_deref()).await {
826 warn!(route_id = %route_id, error = %e, "Failed to unregister function during route removal");
827 }
828 }
829 }
830 self.routes.remove(route_id);
831 if let Some(reg) = &self.health_registry {
832 reg.unregister_for_route(route_id);
833 }
834 self.endpoint_index.remove(route_id);
835 info!(route_id = %route_id, "Route removed from controller");
836 Ok(())
837 }
838
839 fn collect_function_refs(
840 &self,
841 route_id: &str,
842 ) -> Vec<(camel_api::FunctionId, Option<String>)> {
843 self.function_invoker
844 .as_ref()
845 .map(|invoker| invoker.function_refs_for_route(route_id))
846 .unwrap_or_default()
847 }
848
849 fn discard_function_staging(&self) {
850 if let Some(invoker) = &self.function_invoker {
851 invoker.discard_staging(0);
852 }
853 }
854
855 pub fn route_count(&self) -> usize {
857 self.routes.route_count()
858 }
859
860 pub fn in_flight_count(&self, route_id: &str) -> Option<u64> {
861 self.routes.in_flight_count(route_id)
862 }
863
864 pub fn route_exists(&self, route_id: &str) -> bool {
866 self.routes.route_exists(route_id)
867 }
868
869 pub fn route_ids(&self) -> Vec<String> {
871 self.routes.route_ids()
872 }
873
874 pub fn route_source_hash(&self, route_id: &str) -> Option<u64> {
875 self.routes.route_source_hash(route_id)
876 }
877
878 pub fn auto_startup_route_ids(&self) -> Vec<String> {
880 self.routes.auto_startup_route_ids()
881 }
882
883 pub fn shutdown_route_ids(&self) -> Vec<String> {
885 self.routes.shutdown_route_ids()
886 }
887
888 pub fn swap_pipeline(
907 &self,
908 route_id: &str,
909 new_pipeline: BoxProcessor,
910 ) -> Result<(), CamelError> {
911 let managed = self
912 .routes
913 .get(route_id)
914 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
915
916 let assembly = managed.pipeline.load();
917 let has_lifecycle = !assembly.lifecycle.is_empty();
918
919 if has_lifecycle || managed.agg_service.is_some() {
920 warn!(
921 route_id = %route_id,
922 "Hot-swap rejected — route has lifecycle/agg steps; use Restart path"
923 );
924 return Err(CamelError::RouteError(format!(
925 "Route '{}' contains stateful steps (lifecycle-bearing). Hot-swap not supported — use restart.",
926 route_id
927 )));
928 }
929
930 drop(assembly);
931
932 if managed.aggregate_split.is_some() {
933 warn!(
934 route_id = %route_id,
935 "swap_pipeline: aggregate routes with timeout do not support hot-reload of pre/post segments"
936 );
937 }
938
939 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, vec![]);
940 debug!(route_id = %route_id, "Pipeline swapped atomically");
941 Ok(())
942 }
943
944 pub(crate) fn swap_pipeline_raw(
954 &self,
955 route_id: &str,
956 new_pipeline: BoxProcessor,
957 lifecycle: Vec<Arc<dyn StepLifecycle>>,
958 ) -> Result<(), CamelError> {
959 let managed = self
960 .routes
961 .get(route_id)
962 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
963 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, lifecycle);
964 debug!(route_id = %route_id, "Pipeline swapped (raw — lifecycle bypass)");
965 Ok(())
966 }
967
968 pub fn route_from_uri(&self, route_id: &str) -> Option<String> {
970 self.routes.route_from_uri(route_id)
971 }
972
973 pub fn routes_for_endpoint(&self, uri: &str) -> Vec<String> {
975 self.endpoint_index.routes_for(uri)
976 }
977
978 pub fn list_endpoint_uris(&self) -> Vec<String> {
980 self.endpoint_index.list_uris()
981 }
982
983 pub fn get_pipeline(&self, route_id: &str) -> Option<BoxProcessor> {
988 self.routes.get_pipeline(route_id)
989 }
990
991 pub(crate) fn route_has_lifecycle(&self, route_id: &str) -> bool {
995 self.routes
996 .get(route_id)
997 .map(|managed| !managed.pipeline.load().lifecycle.is_empty())
998 .unwrap_or(false)
999 }
1000
1001 pub(super) async fn stop_route_internal(&mut self, route_id: &str) -> Result<(), CamelError> {
1003 self.routes.stop_route(route_id).await
1004 }
1005
1006 pub async fn start_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
1007 self.start_route(route_id).await
1008 }
1009
1010 pub async fn stop_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
1011 self.stop_route(route_id).await
1012 }
1013}
1014
1015impl DefaultRouteController {
1018 #[allow(clippy::too_many_arguments)]
1024 pub(super) async fn start_aggregate_route(
1025 &mut self,
1026 route_id: &str,
1027 split: AggregateSplitInfo,
1028 consumer: Box<dyn Consumer>,
1029 consumer_ctx: ConsumerContext,
1030 mut rx: mpsc::Receiver<ExchangeEnvelope>,
1031 crash_notifier: Option<mpsc::Sender<CrashNotification>>,
1032 runtime_for_consumer: Option<Weak<dyn RuntimeHandle>>,
1033 tx_for_storage: mpsc::Sender<ExchangeEnvelope>,
1034 pipeline_cancel: CancellationToken,
1036 drain_in_flight: Arc<std::sync::atomic::AtomicU64>,
1037 ) -> Result<(), CamelError> {
1038 let (late_tx, late_rx) = mpsc::channel::<Exchange>(256);
1039
1040 let route_cancel_clone = pipeline_cancel.clone();
1041 let mut svc = AggregatorService::new(
1042 split.agg_config.clone(),
1043 late_tx,
1044 Arc::clone(&self.languages),
1045 route_cancel_clone,
1046 );
1047 if let Some(metrics) = self.tracer_metrics.clone() {
1050 svc = svc.with_queue_metrics(metrics, format!("aggregator:{route_id}"));
1051 }
1052 let agg = Arc::new(svc);
1053
1054 let pipeline_cancel_for_monitor = pipeline_cancel.clone();
1055 let mut cohort_rx = self.cohort.subscribe();
1058 let agg_for_monitor = Arc::clone(&agg);
1059
1060 {
1061 let managed = self
1062 .routes
1063 .get_mut(route_id)
1064 .expect("invariant: route must exist"); managed.agg_service = Some(Arc::clone(&agg));
1066 }
1067
1068 let late_rx = Arc::new(tokio::sync::Mutex::new(late_rx));
1069 let pre_pipeline = split.pre_pipeline;
1070 let post_pipeline = split.post_pipeline;
1071
1072 let pipeline_handle = tokio::spawn(async move {
1074 loop {
1075 tokio::select! {
1076 biased;
1077
1078 late_ex = async {
1083 let mut rx = late_rx.lock().await;
1084 rx.recv().await
1085 } => {
1086 match late_ex {
1087 Some(ex) => {
1088 let pipe = post_pipeline.load();
1089 if let Err(e) = pipe.processor.clone_inner().oneshot(ex).await {
1090 tracing::warn!(error = %e, "late exchange post-pipeline failed");
1091 }
1092 }
1093 None => return,
1094 }
1095 }
1096
1097 envelope_opt = rx.recv() => {
1098 match envelope_opt {
1099 Some(envelope) => {
1100 tokio::select! {
1114 biased;
1115 _ = cohort_rx.wait_for(|open| *open) => {}
1116 _ = pipeline_cancel.cancelled() => {
1117 continue;
1130 }
1131 }
1132 let ExchangeEnvelope { exchange, reply_tx } = envelope;
1133 let _drain_guard = super::route_helpers::DrainGuard::new(Arc::clone(&drain_in_flight));
1134 let pre_pipe = pre_pipeline.load();
1135 let ex = match pre_pipe.processor.clone_inner().oneshot(exchange).await {
1136 Ok(ex) => ex,
1137 Err(e) => {
1138 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
1139 continue;
1140 }
1141 };
1142
1143 let ex = {
1144 let cloned_svc = agg.as_ref().clone();
1145 cloned_svc.oneshot(ex).await
1146 };
1147
1148 match ex {
1149 Ok(ex) => {
1150 if !is_pending(&ex) {
1151 let post_pipe = post_pipeline.load();
1152 let out = post_pipe.processor.clone_inner().oneshot(ex).await;
1153 if let Some(tx) = reply_tx { let _ = tx.send(out); }
1154 } else if let Some(tx) = reply_tx {
1155 let _ = tx.send(Ok(ex));
1156 }
1157 }
1158 Err(e) => {
1159 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
1160 }
1161 }
1162 }
1163 None => return,
1164 }
1165 }
1166
1167 _ = pipeline_cancel.cancelled() => {
1168 agg.force_complete_all();
1169 let mut rx_guard = late_rx.lock().await;
1170 while let Ok(late_ex) = rx_guard.try_recv() {
1171 let pipe = post_pipeline.load();
1172 let _ = pipe.processor.clone_inner().oneshot(late_ex).await;
1173 }
1174 break;
1175 }
1176 }
1177 }
1178 });
1179 #[cfg(test)]
1180 emit_start_route_event("pipeline_spawned", route_id);
1181
1182 let consumer_cancel_for_cleanup = consumer_ctx.cancel_token();
1188 let (consumer_handle, startup_rx, watcher_inputs, outer_inputs) =
1189 super::consumer_management::spawn_consumer_task(
1190 route_id.to_string(),
1191 consumer,
1192 consumer_ctx,
1193 crash_notifier,
1194 runtime_for_consumer,
1195 false,
1196 );
1197
1198 let startup_result =
1202 super::consumer_management::await_consumer_startup(startup_rx, "startup").await;
1203 if let Err(e) = startup_result {
1208 consumer_handle.abort();
1209 pipeline_cancel_for_monitor.cancel();
1210 consumer_cancel_for_cleanup.cancel();
1212 return Err(e);
1213 }
1214
1215 if let Some(inputs) = watcher_inputs {
1217 super::consumer_management::spawn_failure_watcher(inputs);
1218 }
1219
1220 if let Some(outer) = outer_inputs {
1226 super::consumer_management::spawn_outer_task_watcher(outer);
1227 }
1228
1229 let force_on_stop = agg_for_monitor.config().force_completion_on_stop;
1233 let consumer_handle = tokio::spawn(async move {
1234 let _ = consumer_handle.await;
1235 if !pipeline_cancel_for_monitor.is_cancelled() {
1236 agg_for_monitor.force_complete_all();
1237 if force_on_stop {
1238 pipeline_cancel_for_monitor.cancel();
1239 }
1240 }
1241 });
1242 #[cfg(test)]
1243 emit_start_route_event("consumer_spawned", route_id);
1244
1245 {
1246 let managed = self
1247 .routes
1248 .get_mut(route_id)
1249 .expect("invariant: route must exist"); managed.consumer_handle = Some(consumer_handle);
1251 managed.pipeline_handle = Some(pipeline_handle);
1252 managed.channel_sender = Some(tx_for_storage);
1253 }
1254
1255 info!(route_id = %route_id, "Route started (aggregate with timeout)");
1256 Ok(())
1257 }
1258
1259 #[cfg(test)]
1264 pub(crate) fn set_route_lifecycle_for_test(
1265 &mut self,
1266 route_id: &str,
1267 lifecycle: Vec<Arc<dyn StepLifecycle>>,
1268 ) -> Result<(), CamelError> {
1269 use super::pipeline_runtime::PipelineAssembly;
1270 use camel_api::SyncBoxProcessor;
1271 use std::sync::Arc;
1272
1273 let managed = self
1274 .routes
1275 .get_mut(route_id)
1276 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
1277 let old_processor = managed.pipeline.load().processor.clone_inner();
1278 managed.pipeline.store(Arc::new(PipelineAssembly::new(
1279 SyncBoxProcessor::new(old_processor),
1280 lifecycle,
1281 )));
1282 Ok(())
1283 }
1284}
1285
1286#[cfg(test)]
1287impl crate::hot_reload::ports::ReloadIntrospectionPort for DefaultRouteController {
1288 fn route_ids(&self) -> Vec<String> {
1289 self.route_ids() }
1291 fn route_from_uri(&self, route_id: &str) -> Option<String> {
1292 self.route_from_uri(route_id) }
1294 fn route_source_hash(&self, route_id: &str) -> Option<u64> {
1295 self.route_source_hash(route_id) }
1297}
1298
1299#[cfg(test)]
1300#[path = "route_controller_tests.rs"]
1301mod tests;
1302
1303#[cfg(test)]
1304#[path = "cohort_activation_regression.rs"]
1305mod cohort_activation_regression;