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, InFlightClaim,
23 InFlightGauge, NoOpMetrics, NoopPlatformService, PlatformService, ProducerContext,
24 RouteController, RuntimeHandle, StepLifecycle,
25};
26use camel_component_api::{Consumer, ConsumerContext, consumer::ExchangeEnvelope};
27use camel_processor::aggregator::AggregatorService;
28pub use camel_processor::aggregator::SharedLanguageRegistry;
29use camel_processor::aggregator::{AggregateEmission, AggregationReceipt};
30
31use crate::health_registry::HealthCheckRegistry;
32use crate::intercept::InterceptRules;
33use crate::lifecycle::CohortActivationGate;
34use crate::lifecycle::adapters::controller_component_context::ControllerComponentContext;
35use crate::lifecycle::adapters::route_compiler::TracerPipelineGating;
36use crate::lifecycle::adapters::route_compiler_ext::{
37 RouteCompilerExt, build_eh_config_pipeline, transport_from_uri,
38};
39use crate::lifecycle::adapters::route_helpers::{
40 AggregateSplitInfo, CrashNotification, ManagedRoute, assert_no_mixed_top_level_splits,
41 handle_is_running, inferred_lifecycle_label, is_pending, send_reply_or_b_prime,
42};
43#[cfg(test)]
44pub(super) use crate::lifecycle::adapters::route_helpers::{
45 emit_start_route_event, set_start_route_event_hook,
46};
47use crate::lifecycle::adapters::route_registry::RouteRegistry;
48use crate::lifecycle::adapters::route_runtime_state;
49use crate::lifecycle::adapters::step_compilers::CompiledStep;
50use crate::lifecycle::application::route_definition::{BuilderStep, RouteDefinition};
51pub(crate) use crate::lifecycle::domain::CompiledPipeline;
52use crate::shared::components::domain::Registry;
53use crate::shared::observability::domain::{DetailLevel, TracerConfig};
54use camel_bean::BeanRegistry;
55
56pub struct DefaultRouteController {
64 pub(super) routes: RouteRegistry,
66 pub(super) registry: Arc<std::sync::Mutex<Registry>>,
68 pub(super) languages: SharedLanguageRegistry,
70 pub(super) beans: Arc<std::sync::Mutex<BeanRegistry>>,
72 pub(super) runtime: Option<Weak<dyn RuntimeHandle>>,
74 pub(super) global_error_handler: Option<ErrorHandlerConfig>,
76 pub(super) crash_notifier: Option<mpsc::Sender<CrashNotification>>,
78 pub(super) tracer_gating: TracerPipelineGating,
80 pub(super) tracer_detail_level: DetailLevel,
82 pub(super) tracer_metrics: Option<Arc<dyn MetricsCollector>>,
84 pub(super) platform_service: Arc<dyn PlatformService>,
85 pub(super) function_invoker: Option<Arc<dyn FunctionInvoker>>,
86 pub(super) health_registry: Option<Arc<HealthCheckRegistry>>,
87 pub(super) idempotent_repositories: crate::SharedIdempotentRegistry,
91 pub(super) claim_check_repositories: crate::SharedClaimCheckRegistry,
92 pub(super) cache_repositories: crate::SharedCacheRegistry,
93 pub(super) prepared_staging: HashMap<String, ManagedRoute>,
98 pub(super) endpoint_index: super::endpoint_index::EndpointIndex,
100 pub(super) bind_acks: super::route_controller_trait::BindExposureAcks,
103 pub(super) intercept: InterceptRules,
105 pub(super) frozen: bool,
109 pub(super) cohort: Arc<CohortActivationGate>,
113 pub(super) in_flight_total: Arc<InFlightGauge>,
119}
120
121impl DefaultRouteController {
122 pub fn activate_cohort(&self) {
131 self.cohort.open();
132 }
133
134 pub(super) fn health_registry(&self) -> Arc<HealthCheckRegistry> {
135 self.health_registry.clone().unwrap_or_else(|| {
136 debug!("health_registry not configured — creating isolated fallback");
137 Arc::new(HealthCheckRegistry::new(Duration::from_secs(5)))
138 })
139 }
140
141 pub fn new(
143 registry: Arc<std::sync::Mutex<Registry>>,
144 platform_service: Arc<dyn PlatformService>,
145 ) -> Self {
146 Self::with_beans_and_platform_service(
147 registry,
148 Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
149 platform_service,
150 )
151 }
152
153 pub fn with_beans(
155 registry: Arc<std::sync::Mutex<Registry>>,
156 beans: Arc<std::sync::Mutex<BeanRegistry>>,
157 ) -> Self {
158 Self::with_beans_and_platform_service(
159 registry,
160 beans,
161 Arc::new(NoopPlatformService::default()),
162 )
163 }
164
165 fn with_beans_and_platform_service(
166 registry: Arc<std::sync::Mutex<Registry>>,
167 beans: Arc<std::sync::Mutex<BeanRegistry>>,
168 platform_service: Arc<dyn PlatformService>,
169 ) -> Self {
170 Self {
171 routes: RouteRegistry::new(),
172 registry,
173 languages: Arc::new(std::sync::Mutex::new(HashMap::new())),
174 beans,
175 runtime: None,
176 global_error_handler: None,
177 crash_notifier: None,
178 tracer_gating: TracerPipelineGating::off(),
179 tracer_detail_level: DetailLevel::Minimal,
180 tracer_metrics: None,
181 platform_service,
182 function_invoker: None,
183 health_registry: None,
184 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
185 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
186 cache_repositories: Arc::new(crate::CacheRegistry::new()),
187 prepared_staging: HashMap::new(),
188 endpoint_index: super::endpoint_index::EndpointIndex::new(),
189 bind_acks: Default::default(),
190 intercept: InterceptRules::default(),
191 frozen: false,
192 cohort: Arc::new(CohortActivationGate::new_closed()),
193 in_flight_total: Arc::new(InFlightGauge::new()),
194 }
195 }
196
197 pub fn with_languages(
199 registry: Arc<std::sync::Mutex<Registry>>,
200 languages: SharedLanguageRegistry,
201 platform_service: Arc<dyn PlatformService>,
202 ) -> Self {
203 Self {
204 routes: RouteRegistry::new(),
205 registry,
206 languages,
207 beans: Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
208 runtime: None,
209 global_error_handler: None,
210 crash_notifier: None,
211 tracer_gating: TracerPipelineGating::off(),
212 tracer_detail_level: DetailLevel::Minimal,
213 tracer_metrics: None,
214 platform_service,
215 function_invoker: None,
216 health_registry: None,
217 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
218 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
219 cache_repositories: Arc::new(crate::CacheRegistry::new()),
220 prepared_staging: HashMap::new(),
221 endpoint_index: super::endpoint_index::EndpointIndex::new(),
222 bind_acks: Default::default(),
223 intercept: InterceptRules::default(),
224 frozen: false,
225 cohort: Arc::new(CohortActivationGate::new_closed()),
226 in_flight_total: Arc::new(InFlightGauge::new()),
227 }
228 }
229
230 pub fn with_languages_and_beans(
231 registry: Arc<std::sync::Mutex<Registry>>,
232 languages: SharedLanguageRegistry,
233 platform_service: Arc<dyn PlatformService>,
234 beans: Arc<std::sync::Mutex<BeanRegistry>>,
235 ) -> Self {
236 Self {
237 routes: RouteRegistry::new(),
238 registry,
239 languages,
240 beans,
241 runtime: None,
242 global_error_handler: None,
243 crash_notifier: None,
244 tracer_gating: TracerPipelineGating::off(),
245 tracer_detail_level: DetailLevel::Minimal,
246 tracer_metrics: None,
247 platform_service,
248 function_invoker: None,
249 health_registry: None,
250 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
251 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
252 cache_repositories: Arc::new(crate::CacheRegistry::new()),
253 prepared_staging: HashMap::new(),
254 endpoint_index: super::endpoint_index::EndpointIndex::new(),
255 bind_acks: Default::default(),
256 intercept: InterceptRules::default(),
257 frozen: false,
258 cohort: Arc::new(CohortActivationGate::new_closed()),
259 in_flight_total: Arc::new(InFlightGauge::new()),
260 }
261 }
262
263 pub fn with_function_invoker(mut self, function_invoker: Arc<dyn FunctionInvoker>) -> Self {
264 self.function_invoker = Some(function_invoker);
265 self
266 }
267
268 pub(crate) fn set_idempotent_repositories(
269 &mut self,
270 repositories: crate::SharedIdempotentRegistry,
271 ) {
272 self.idempotent_repositories = repositories;
273 }
274
275 pub(crate) fn set_claim_check_repositories(
276 &mut self,
277 repositories: crate::SharedClaimCheckRegistry,
278 ) {
279 self.claim_check_repositories = repositories;
280 }
281
282 pub(crate) fn set_cache_repositories(&mut self, repositories: crate::SharedCacheRegistry) {
283 self.cache_repositories = repositories;
284 }
285
286 pub fn set_health_registry(&mut self, registry: Arc<HealthCheckRegistry>) {
287 self.health_registry = Some(registry);
288 }
289
290 pub fn set_function_invoker(&mut self, invoker: Arc<dyn FunctionInvoker>) {
291 self.function_invoker = Some(invoker);
292 }
293
294 pub fn set_runtime_handle(&mut self, runtime: Arc<dyn RuntimeHandle>) {
296 self.runtime = Some(Arc::downgrade(&runtime));
297 }
298
299 pub fn set_crash_notifier(&mut self, tx: mpsc::Sender<CrashNotification>) {
304 self.crash_notifier = Some(tx);
305 }
306
307 pub fn set_bind_exposure_acks(&mut self, acks: BindExposureAcks) {
311 self.bind_acks = acks;
312 }
313
314 pub fn set_intercept_rules(&mut self, rules: InterceptRules) -> Result<(), CamelError> {
320 if self.frozen {
321 return Err(CamelError::Config(
322 "intercept rules are frozen: a route was added or the context was started; \
323 rules cannot be changed after first use"
324 .into(),
325 ));
326 }
327 self.intercept = rules;
328 Ok(())
329 }
330
331 pub fn with_intercept_rules(mut self, rules: InterceptRules) -> Self {
334 self.intercept = rules;
335 self
336 }
337
338 pub fn mark_started(&mut self) {
342 self.frozen = true;
343 }
344
345 pub(super) fn plans_for_bind(
350 &self,
351 bind_key: &str,
352 ) -> Vec<(String, camel_api::security_policy::RouteSecurityPlan)> {
353 self.routes
354 .iter()
355 .filter_map(|(route_id, managed)| {
356 let plan = managed.compiled.security_plan.as_ref()?;
357 let bind = bind_key_from_uri(&managed.from_uri)?;
358 if bind.key == bind_key {
359 Some((route_id.clone(), plan.clone()))
360 } else {
361 None
362 }
363 })
364 .collect()
365 }
366
367 fn bind_is_running(&self, bind_key: &str) -> bool {
374 self.routes.iter().any(|(_, managed)| {
375 bind_key_from_uri(&managed.from_uri).is_some_and(|bind| {
376 bind.key == bind_key && handle_is_running(&managed.consumer_handle)
377 })
378 })
379 }
380
381 fn enforce_late_registration_gate(
385 &self,
386 from_uri: &str,
387 route_id: &str,
388 plan: Option<&camel_api::security_policy::RouteSecurityPlan>,
389 ) -> Result<(), CamelError> {
390 let Some(bind) = bind_key_from_uri(from_uri) else {
391 return Ok(());
392 };
393 if !self.bind_is_running(&bind.key) {
394 return Ok(());
395 }
396
397 let mut owned = self.plans_for_bind(&bind.key);
398 if let Some(plan) = plan {
399 owned.push((route_id.to_string(), plan.clone()));
400 }
401 let plans: Vec<(&str, &camel_api::security_policy::RouteSecurityPlan)> =
402 owned.iter().map(|(id, plan)| (id.as_str(), plan)).collect();
403 enforce_bind_exposure_gate(
404 &bind.key,
405 bind.loopback,
406 &plans,
407 self.bind_acks.acknowledged(&bind.key),
408 )
409 .map_err(|err| {
410 CamelError::RouteError(format!(
411 "late registration of route '{route_id}' rejected for bind '{}': {err}",
412 bind.key
413 ))
414 })
415 }
416
417 pub fn set_error_handler(&mut self, config: ErrorHandlerConfig) {
418 self.global_error_handler = Some(config);
419 }
420
421 pub fn set_tracer_config(&mut self, config: &TracerConfig) {
423 self.tracer_gating = TracerPipelineGating {
426 pipeline_enabled: config.enabled || config.pipeline_enabled,
427 spans_enabled: config.enabled,
428 levers: config.metrics_levers.clone(),
429 };
430 self.tracer_detail_level = config.detail_level.clone();
431 }
432
433 pub fn set_tracer_metrics(&mut self, metrics: Arc<dyn MetricsCollector>) {
440 self.tracer_metrics = Some(metrics);
441 }
442
443 pub fn set_in_flight_total(&mut self, counter: Arc<InFlightGauge>) {
450 self.in_flight_total = counter;
451 }
452
453 fn build_producer_context(&self, route_id: &str) -> Result<ProducerContext, CamelError> {
454 let mut producer_ctx = ProducerContext::new().with_route_id(route_id);
455 if let Some(runtime) = self.runtime.as_ref().and_then(Weak::upgrade) {
456 producer_ctx = producer_ctx.with_runtime(runtime);
457 }
458 Ok(producer_ctx)
459 }
460
461 fn route_compiler_ext(&self) -> RouteCompilerExt<'_> {
463 RouteCompilerExt {
464 registry: &self.registry,
465 languages: &self.languages,
466 beans: &self.beans,
467 function_invoker: &self.function_invoker,
468 tracer_gating: self.tracer_gating.clone(),
469 tracer_detail_level: &self.tracer_detail_level,
470 tracer_metrics: &self.tracer_metrics,
471 platform_service: &self.platform_service,
472 runtime: &self.runtime,
473 global_error_handler: &self.global_error_handler,
474 health_registry: &self.health_registry,
475 route_registry: &self.routes,
476 idempotent_repositories: Arc::clone(&self.idempotent_repositories),
477 claim_check_repositories: Arc::clone(&self.claim_check_repositories),
478 cache_repositories: Arc::clone(&self.cache_repositories),
479 intercept: &self.intercept,
480 in_flight_total: Arc::clone(&self.in_flight_total),
481 }
482 }
483
484 #[allow(dead_code)] pub(crate) fn resolve_steps(
487 &self,
488 steps: Vec<BuilderStep>,
489 producer_ctx: &ProducerContext,
490 registry: &Arc<std::sync::Mutex<Registry>>,
491 route_id: Option<&str>,
492 staging_mode: &super::step_resolution::FunctionStagingMode,
493 ) -> Result<Vec<CompiledStep>, CamelError> {
494 let component_ctx = Arc::new(ControllerComponentContext::new(
495 Arc::clone(registry),
496 Arc::clone(&self.languages),
497 self.tracer_metrics
498 .clone()
499 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
500 Arc::clone(&self.platform_service),
501 self.health_registry(),
502 route_id.map(|s| s.to_string()),
503 self.tracer_gating.levers.components_enabled(),
504 ));
505 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
506 Arc::clone(&component_ctx) as Arc<_>;
507
508 super::step_resolution::resolve_steps(
509 steps,
510 producer_ctx,
511 rt,
512 registry,
513 &self.languages,
514 &self.beans,
515 self.function_invoker.clone(),
516 component_ctx,
517 route_id,
518 staging_mode,
519 &self.idempotent_repositories,
520 &self.claim_check_repositories,
521 &self.cache_repositories,
522 self.intercept.clone(),
523 )
524 }
525
526 pub async fn add_route(&mut self, definition: RouteDefinition) -> Result<(), CamelError> {
536 let route_id = definition.route_id().to_string();
537 let from_uri = definition.from_uri().to_string();
538
539 if self.routes.contains_key(&route_id) {
540 return Err(CamelError::RouteError(format!(
541 "duplicate route ID '{route_id}'"
542 )));
543 }
544
545 debug!(route_id = %route_id, "Adding route to controller");
546
547 let managed = match self.build_managed_route(
548 definition,
549 &super::step_resolution::FunctionStagingMode::DirectAdd,
550 ) {
551 Ok(managed) => managed,
552 Err(err) => {
553 self.discard_function_staging();
554 return Err(err);
555 }
556 };
557
558 if let Err(err) = self.enforce_late_registration_gate(
562 &from_uri,
563 &route_id,
564 managed.compiled.security_plan.as_ref(),
565 ) {
566 self.discard_function_staging();
567 return Err(err);
568 }
569
570 if let Some(invoker) = &self.function_invoker
571 && let Err(err) = invoker.commit_staged().await
572 {
573 invoker.discard_staging(0);
574 return Err(CamelError::Config(err.to_string()));
575 }
576
577 self.routes
578 .insert(managed.definition.route_id().to_string(), managed);
579
580 self.endpoint_index.insert(&from_uri, &route_id);
581 self.frozen = true;
584 Ok(())
585 }
586
587 pub(super) fn build_managed_route(
588 &self,
589 definition: RouteDefinition,
590 staging_mode: &super::step_resolution::FunctionStagingMode,
591 ) -> Result<ManagedRoute, CamelError> {
592 let route_id = definition.route_id().to_string();
593
594 let definition_info = definition.to_info();
595
596 let empty_providers;
601 let providers = match &definition.provider_registry {
602 Some(registry) => registry.as_ref(),
603 None => {
604 empty_providers = camel_auth::ProviderRegistry::new();
605 &empty_providers
606 }
607 };
608 let security_plan =
609 super::route_compiler_ext::compile_route_security_plan(&definition, providers)?;
610
611 let RouteDefinition {
612 from_uri,
613 steps,
614 error_handler,
615 circuit_breaker,
616 circuit_breaker_fallback,
617 security_policy,
618 security_authenticator,
619 provider_registry,
620 unit_of_work,
621 concurrency,
622 ..
623 } = definition;
624
625 let producer_ctx = self.build_producer_context(&route_id)?;
626
627 assert_no_mixed_top_level_splits(&steps)?;
629
630 let (aggregate_split, processors_with_contracts) = self
631 .route_compiler_ext()
632 .detect_and_validate_route_split(steps, &producer_ctx, &route_id, staging_mode)?;
633 let mut lifecycle = super::route_helpers::collect_lifecycle(&processors_with_contracts);
634
635 let (circuit_breaker, fallback_lifecycle) = self.route_compiler_ext().attach_cb_fallback(
639 circuit_breaker,
640 circuit_breaker_fallback,
641 &producer_ctx,
642 &route_id,
643 staging_mode,
644 )?;
645 lifecycle.extend(fallback_lifecycle);
646 let route_id_for_tracing = route_id.clone();
647 let eh_config = error_handler.or_else(|| self.global_error_handler.clone());
648 let transport = transport_from_uri(&from_uri);
649
650 let mut pipeline = build_eh_config_pipeline(
651 eh_config.as_ref(),
652 Arc::clone(&self.registry),
653 Arc::clone(&self.languages),
654 self.tracer_metrics.clone(),
655 Arc::clone(&self.platform_service),
656 self.health_registry(),
657 &route_id_for_tracing,
658 &producer_ctx,
659 processors_with_contracts,
660 self.tracer_gating.clone(),
661 self.tracer_detail_level.clone(),
662 security_policy.clone(),
663 transport,
664 circuit_breaker,
665 Arc::clone(&self.in_flight_total),
666 )?;
667
668 let uow_counter = if let Some(uow_config) = &unit_of_work {
669 let component_ctx = Arc::new(
670 ControllerComponentContext::new(
671 Arc::clone(&self.registry),
672 Arc::clone(&self.languages),
673 self.tracer_metrics
674 .clone()
675 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
676 Arc::clone(&self.platform_service),
677 self.health_registry(),
678 Some(route_id.clone()),
679 self.tracer_gating.levers.components_enabled(),
680 )
681 .with_in_flight(Arc::clone(&self.in_flight_total)),
682 );
683 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
684 Arc::clone(&component_ctx) as Arc<_>;
685 let (uow_layer, counter) = super::route_compiler_ext::resolve_uow_layer(
686 uow_config,
687 &producer_ctx,
688 rt,
689 component_ctx.as_ref(),
690 None,
691 )?;
692 pipeline = BoxProcessor::new(uow_layer.layer(pipeline));
693 Some(counter)
694 } else {
695 None
696 };
697
698 Ok(ManagedRoute {
699 definition: definition_info,
700 from_uri,
701 pipeline: super::pipeline_runtime::new_shared_pipeline_with_lifecycle(
702 pipeline, lifecycle,
703 ),
704 concurrency,
705 consumer_handle: None,
706 pipeline_handle: None,
707 consumer_cancel_token: CancellationToken::new(),
708 pipeline_cancel_token: CancellationToken::new(),
709 channel_sender: None,
710 in_flight: uow_counter,
711 drain_in_flight: Arc::new(std::sync::atomic::AtomicU64::new(0)),
712 aggregate_split,
713 agg_service: None,
714 stop_budget: None,
715 compiled: route_runtime_state::CompiledRoute {
716 security_policy,
717 security_authenticator,
718 provider_registry,
719 security_plan,
720 },
721 })
722 }
723
724 pub async fn add_route_with_generation(
725 &mut self,
726 definition: RouteDefinition,
727 generation: u64,
728 ) -> Result<(), CamelError> {
729 let route_id = definition.route_id().to_string();
730 let from_uri = definition.from_uri().to_string();
731
732 if self.routes.contains_key(&route_id) {
733 return Err(CamelError::RouteError(format!(
734 "duplicate route ID '{route_id}'"
735 )));
736 }
737
738 debug!(route_id = %route_id, generation, "Adding route to controller with generation");
739
740 let managed = self.build_managed_route(
741 definition,
742 &super::step_resolution::FunctionStagingMode::HotReload { generation },
743 )?;
744
745 if let Err(err) = self.enforce_late_registration_gate(
748 &from_uri,
749 &route_id,
750 managed.compiled.security_plan.as_ref(),
751 ) {
752 self.discard_function_staging();
753 return Err(err);
754 }
755
756 self.routes.insert(route_id.clone(), managed);
757
758 self.endpoint_index.insert(&from_uri, &route_id);
759 Ok(())
760 }
761
762 pub async fn remove_route_preserving_functions(
763 &mut self,
764 route_id: &str,
765 ) -> Result<(), CamelError> {
766 let managed = self.routes.get(route_id).ok_or_else(|| {
767 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
768 })?;
769 if handle_is_running(&managed.consumer_handle)
770 || handle_is_running(&managed.pipeline_handle)
771 {
772 return Err(CamelError::RouteError(format!(
773 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
774 route_id,
775 inferred_lifecycle_label(managed)
776 )));
777 }
778 self.routes.remove(route_id);
779 if let Some(reg) = &self.health_registry {
780 reg.unregister_for_route(route_id);
781 }
782 self.endpoint_index.remove(route_id);
783 debug!(route_id = %route_id, "Route removed from controller (functions preserved for reload finalize)");
784 Ok(())
785 }
786
787 pub fn compile_route_definition(
790 &self,
791 def: RouteDefinition,
792 ) -> Result<BoxProcessor, CamelError> {
793 self.route_compiler_ext().compile_route_definition(def)
794 }
795
796 pub fn compile_route_definition_with_generation(
798 &self,
799 def: RouteDefinition,
800 generation: u64,
801 ) -> Result<BoxProcessor, CamelError> {
802 self.route_compiler_ext()
803 .compile_route_definition_with_generation(def, generation)
804 }
805
806 pub(crate) fn compile_route_definition_pipeline(
811 &self,
812 def: RouteDefinition,
813 generation: u64,
814 ) -> Result<CompiledPipeline, CamelError> {
815 self.route_compiler_ext()
816 .compile_route_definition_pipeline(def, generation)
817 }
818
819 pub(crate) fn compile_route_definition_dry_pipeline(
824 &self,
825 def: RouteDefinition,
826 ) -> Result<CompiledPipeline, CamelError> {
827 self.route_compiler_ext()
828 .compile_route_definition_dry_pipeline(def)
829 }
830
831 pub async fn remove_route(&mut self, route_id: &str) -> Result<(), CamelError> {
837 let managed = self.routes.get(route_id).ok_or_else(|| {
838 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
839 })?;
840 if handle_is_running(&managed.consumer_handle)
841 || handle_is_running(&managed.pipeline_handle)
842 {
843 return Err(CamelError::RouteError(format!(
844 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
845 route_id,
846 inferred_lifecycle_label(managed)
847 )));
848 }
849 if let Some(invoker) = &self.function_invoker {
850 for (id, rid) in self.collect_function_refs(route_id) {
851 if let Err(e) = invoker.unregister(&id, rid.as_deref()).await {
852 warn!(route_id = %route_id, error = %e, "Failed to unregister function during route removal");
853 }
854 }
855 }
856 self.routes.remove(route_id);
857 if let Some(reg) = &self.health_registry {
858 reg.unregister_for_route(route_id);
859 }
860 self.endpoint_index.remove(route_id);
861 info!(route_id = %route_id, "Route removed from controller");
862 Ok(())
863 }
864
865 fn collect_function_refs(
866 &self,
867 route_id: &str,
868 ) -> Vec<(camel_api::FunctionId, Option<String>)> {
869 self.function_invoker
870 .as_ref()
871 .map(|invoker| invoker.function_refs_for_route(route_id))
872 .unwrap_or_default()
873 }
874
875 fn discard_function_staging(&self) {
876 if let Some(invoker) = &self.function_invoker {
877 invoker.discard_staging(0);
878 }
879 }
880
881 pub fn route_count(&self) -> usize {
883 self.routes.route_count()
884 }
885
886 pub fn in_flight_count(&self, route_id: &str) -> Option<u64> {
887 self.routes.in_flight_count(route_id)
888 }
889
890 pub fn route_exists(&self, route_id: &str) -> bool {
892 self.routes.route_exists(route_id)
893 }
894
895 pub fn route_ids(&self) -> Vec<String> {
897 self.routes.route_ids()
898 }
899
900 pub fn route_source_hash(&self, route_id: &str) -> Option<u64> {
901 self.routes.route_source_hash(route_id)
902 }
903
904 pub fn auto_startup_route_ids(&self) -> Vec<String> {
906 self.routes.auto_startup_route_ids()
907 }
908
909 pub fn shutdown_route_ids(&self) -> Vec<String> {
911 self.routes.shutdown_route_ids()
912 }
913
914 pub fn swap_pipeline(
933 &self,
934 route_id: &str,
935 new_pipeline: BoxProcessor,
936 ) -> Result<(), CamelError> {
937 let managed = self
938 .routes
939 .get(route_id)
940 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
941
942 let assembly = managed.pipeline.load();
943 let has_lifecycle = !assembly.lifecycle.is_empty();
944
945 if has_lifecycle || managed.agg_service.is_some() {
946 warn!(
947 route_id = %route_id,
948 "Hot-swap rejected — route has lifecycle/agg steps; use Restart path"
949 );
950 return Err(CamelError::RouteError(format!(
951 "Route '{}' contains stateful steps (lifecycle-bearing). Hot-swap not supported — use restart.",
952 route_id
953 )));
954 }
955
956 drop(assembly);
957
958 if managed.aggregate_split.is_some() {
959 warn!(
960 route_id = %route_id,
961 "swap_pipeline: aggregate routes with timeout do not support hot-reload of pre/post segments"
962 );
963 }
964
965 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, vec![]);
966 debug!(route_id = %route_id, "Pipeline swapped atomically");
967 Ok(())
968 }
969
970 pub(crate) fn swap_pipeline_raw(
980 &self,
981 route_id: &str,
982 new_pipeline: BoxProcessor,
983 lifecycle: Vec<Arc<dyn StepLifecycle>>,
984 ) -> Result<(), CamelError> {
985 let managed = self
986 .routes
987 .get(route_id)
988 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
989 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, lifecycle);
990 debug!(route_id = %route_id, "Pipeline swapped (raw — lifecycle bypass)");
991 Ok(())
992 }
993
994 pub fn route_from_uri(&self, route_id: &str) -> Option<String> {
996 self.routes.route_from_uri(route_id)
997 }
998
999 pub fn routes_for_endpoint(&self, uri: &str) -> Vec<String> {
1001 self.endpoint_index.routes_for(uri)
1002 }
1003
1004 pub fn list_endpoint_uris(&self) -> Vec<String> {
1006 self.endpoint_index.list_uris()
1007 }
1008
1009 pub fn get_pipeline(&self, route_id: &str) -> Option<BoxProcessor> {
1014 self.routes.get_pipeline(route_id)
1015 }
1016
1017 pub(crate) fn route_has_lifecycle(&self, route_id: &str) -> bool {
1021 self.routes
1022 .get(route_id)
1023 .map(|managed| !managed.pipeline.load().lifecycle.is_empty())
1024 .unwrap_or(false)
1025 }
1026
1027 pub(super) async fn stop_route_internal(&mut self, route_id: &str) -> Result<(), CamelError> {
1029 self.routes.stop_route(route_id).await
1030 }
1031
1032 pub(super) async fn stop_route_with_budget_internal(
1034 &mut self,
1035 route_id: &str,
1036 budget: std::time::Duration,
1037 ) -> Result<(), CamelError> {
1038 self.routes.stop_route_with_budget(route_id, budget).await
1039 }
1040
1041 pub async fn start_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
1042 self.start_route(route_id).await
1043 }
1044
1045 pub async fn stop_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
1046 self.stop_route(route_id).await
1047 }
1048}
1049
1050impl DefaultRouteController {
1053 #[allow(clippy::too_many_arguments)]
1075 pub(super) async fn start_aggregate_route(
1076 &mut self,
1077 route_id: &str,
1078 split: AggregateSplitInfo,
1079 consumer: Box<dyn Consumer>,
1080 consumer_ctx: ConsumerContext,
1081 mut rx: mpsc::Receiver<ExchangeEnvelope>,
1082 crash_notifier: Option<mpsc::Sender<CrashNotification>>,
1083 runtime_for_consumer: Option<Weak<dyn RuntimeHandle>>,
1084 tx_for_storage: mpsc::Sender<ExchangeEnvelope>,
1085 pipeline_cancel: CancellationToken,
1087 drain_in_flight: Arc<std::sync::atomic::AtomicU64>,
1088 ) -> Result<(), CamelError> {
1089 let (late_tx, late_rx) =
1093 mpsc::channel::<camel_processor::aggregator::AggregateEmission>(256);
1094
1095 let route_cancel_clone = pipeline_cancel.clone();
1096 let mut svc = AggregatorService::new(
1097 split.agg_config.clone(),
1098 late_tx,
1099 Arc::clone(&self.languages),
1100 route_cancel_clone,
1101 );
1102 if let Some(metrics) = self.tracer_metrics.clone() {
1105 svc = svc.with_queue_metrics(metrics, format!("aggregator:{route_id}"));
1106 }
1107 let agg = Arc::new(svc);
1108
1109 let pipeline_cancel_for_monitor = pipeline_cancel.clone();
1110 let route_id_for_metrics = route_id.to_string();
1113 let metrics_for_reply_drop = self.tracer_metrics.clone();
1114 let mut cohort_rx = self.cohort.subscribe();
1117 let agg_for_monitor = Arc::clone(&agg);
1118
1119 {
1120 let managed = self
1121 .routes
1122 .get_mut(route_id)
1123 .expect("invariant: route must exist"); managed.agg_service = Some(Arc::clone(&agg));
1125 }
1126
1127 let late_rx = Arc::new(tokio::sync::Mutex::new(late_rx));
1128 let pre_pipeline = split.pre_pipeline;
1129 let post_pipeline = split.post_pipeline;
1130
1131 let pipeline_handle = tokio::spawn(async move {
1133 loop {
1134 tokio::select! {
1135 biased;
1136
1137 late_ex = async {
1142 let mut rx = late_rx.lock().await;
1143 rx.recv().await
1144 } => {
1145 match late_ex {
1146 Some(emission) => {
1147 let AggregateEmission {
1151 mut exchange,
1152 claims: _in_flight_claims,
1153 } = emission;
1154 exchange.in_flight_claim = _in_flight_claims
1160 .iter()
1161 .flatten()
1162 .next()
1163 .map(InFlightClaim::split);
1164 let pipe = post_pipeline.load();
1165 if let Err(e) =
1166 pipe.processor.clone_inner().oneshot(exchange).await
1167 {
1168 tracing::warn!(error = %e, "late exchange post-pipeline failed");
1169 }
1170 }
1171 None => return,
1172 }
1173 }
1174
1175 envelope_opt = rx.recv() => {
1176 match envelope_opt {
1177 Some(envelope) => {
1178 tokio::select! {
1192 biased;
1193 _ = cohort_rx.wait_for(|open| *open) => {}
1194 _ = pipeline_cancel.cancelled() => {
1195 continue;
1208 }
1209 }
1210 let ExchangeEnvelope {
1211 exchange,
1212 reply_tx,
1213 in_flight_claim,
1214 } = envelope;
1215 let _drain_guard = super::route_helpers::DrainGuard::new(Arc::clone(&drain_in_flight));
1216 let pre_pipe = pre_pipeline.load();
1227 let mut exchange = exchange;
1234 exchange.in_flight_claim = in_flight_claim.as_ref().map(InFlightClaim::split);
1235 let ex = match pre_pipe.processor.clone_inner().oneshot(exchange).await {
1236 Ok(mut ex) => {
1239 ex.in_flight_claim = None;
1240 ex
1241 }
1242 Err(e) => {
1243 send_reply_or_b_prime(
1247 reply_tx,
1248 Err(e),
1249 &metrics_for_reply_drop,
1250 &route_id_for_metrics,
1251 "aggregate:pre-pipeline",
1252 );
1253 continue;
1254 }
1255 };
1256
1257 let AggregationReceipt { reply, claims } =
1258 agg.submit_with_claim(ex, in_flight_claim).await;
1259 let _completed_bucket_claims = claims;
1264
1265 match reply {
1266 Ok(ex) => {
1267 if !is_pending(&ex) {
1268 let post_pipe = post_pipeline.load();
1269 let mut ex = ex;
1276 ex.in_flight_claim = _completed_bucket_claims
1277 .iter()
1278 .flatten()
1279 .next()
1280 .map(InFlightClaim::split);
1281 let mut out = post_pipe.processor.clone_inner().oneshot(ex).await;
1282 if let Ok(ref mut out_ex) = out {
1283 out_ex.in_flight_claim = None;
1284 }
1285 send_reply_or_b_prime(
1291 reply_tx,
1292 out,
1293 &metrics_for_reply_drop,
1294 &route_id_for_metrics,
1295 "aggregate:post-pipeline",
1296 );
1297 } else {
1298 send_reply_or_b_prime(
1301 reply_tx,
1302 Ok(ex),
1303 &metrics_for_reply_drop,
1304 &route_id_for_metrics,
1305 "aggregate:pending",
1306 );
1307 }
1308 }
1309 Err(e) => {
1310 send_reply_or_b_prime(
1311 reply_tx,
1312 Err(e),
1313 &metrics_for_reply_drop,
1314 &route_id_for_metrics,
1315 "aggregate:pipeline",
1316 );
1317 }
1318 }
1319 }
1320 None => return,
1321 }
1322 }
1323
1324 _ = pipeline_cancel.cancelled() => {
1325 agg.force_complete_all();
1326 let mut rx_guard = late_rx.lock().await;
1327 while let Ok(late_ex) = rx_guard.try_recv() {
1328 let AggregateEmission {
1333 mut exchange,
1334 claims: _in_flight_claims,
1335 } = late_ex;
1336 exchange.in_flight_claim = _in_flight_claims
1341 .iter()
1342 .flatten()
1343 .next()
1344 .map(InFlightClaim::split);
1345 let pipe = post_pipeline.load();
1346 let _ = pipe.processor.clone_inner().oneshot(exchange).await;
1347 }
1348 break;
1349 }
1350 }
1351 }
1352 });
1353 #[cfg(test)]
1354 emit_start_route_event("pipeline_spawned", route_id);
1355
1356 let consumer_cancel_for_cleanup = consumer_ctx.cancel_token();
1362 let (consumer_handle, startup_rx, watcher_inputs, outer_inputs, stop_budget_tx) =
1363 super::consumer_management::spawn_consumer_task(
1364 route_id.to_string(),
1365 consumer,
1366 consumer_ctx,
1367 crash_notifier,
1368 runtime_for_consumer,
1369 false,
1370 );
1371
1372 let startup_result =
1376 super::consumer_management::await_consumer_startup(startup_rx, "startup").await;
1377 if let Err(e) = startup_result {
1382 consumer_handle.abort();
1383 pipeline_cancel_for_monitor.cancel();
1384 consumer_cancel_for_cleanup.cancel();
1386 return Err(e);
1387 }
1388
1389 if let Some(inputs) = watcher_inputs {
1391 super::consumer_management::spawn_failure_watcher(inputs);
1392 }
1393
1394 if let Some(outer) = outer_inputs {
1400 super::consumer_management::spawn_outer_task_watcher(outer);
1401 }
1402
1403 let force_on_stop = agg_for_monitor.config().force_completion_on_stop;
1421 let consumer_handle = tokio::spawn(async move {
1422 let _ = consumer_handle.await;
1423 if !pipeline_cancel_for_monitor.is_cancelled() {
1424 if force_on_stop {
1425 agg_for_monitor.force_complete_all();
1426 pipeline_cancel_for_monitor.cancel();
1427 } else {
1428 agg_for_monitor.release_unarmed_buckets();
1429 }
1430 }
1431 });
1432 #[cfg(test)]
1433 emit_start_route_event("consumer_spawned", route_id);
1434
1435 {
1436 let managed = self
1437 .routes
1438 .get_mut(route_id)
1439 .expect("invariant: route must exist"); managed.consumer_handle = Some(consumer_handle);
1441 managed.pipeline_handle = Some(pipeline_handle);
1442 managed.channel_sender = Some(tx_for_storage);
1443 managed.stop_budget = Some(stop_budget_tx);
1444 }
1445
1446 info!(route_id = %route_id, "Route started (aggregate with timeout)");
1447 Ok(())
1448 }
1449
1450 #[cfg(test)]
1455 pub(crate) fn set_route_lifecycle_for_test(
1456 &mut self,
1457 route_id: &str,
1458 lifecycle: Vec<Arc<dyn StepLifecycle>>,
1459 ) -> Result<(), CamelError> {
1460 use super::pipeline_runtime::PipelineAssembly;
1461 use camel_api::SyncBoxProcessor;
1462 use std::sync::Arc;
1463
1464 let managed = self
1465 .routes
1466 .get_mut(route_id)
1467 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
1468 let old_processor = managed.pipeline.load().processor.clone_inner();
1469 managed.pipeline.store(Arc::new(PipelineAssembly::new(
1470 SyncBoxProcessor::new(old_processor),
1471 lifecycle,
1472 )));
1473 Ok(())
1474 }
1475}
1476
1477#[cfg(test)]
1483impl crate::hot_reload::ports::ReloadIntrospectionPort for DefaultRouteController {
1484 fn route_ids(&self) -> Vec<String> {
1485 self.route_ids() }
1487 fn route_from_uri(&self, route_id: &str) -> Option<String> {
1488 self.route_from_uri(route_id) }
1490 fn route_source_hash(&self, route_id: &str) -> Option<u64> {
1491 self.route_source_hash(route_id) }
1493}
1494
1495#[cfg(test)]
1496#[path = "route_controller_tests.rs"]
1497mod tests;
1498
1499#[cfg(test)]
1500#[path = "cohort_activation_regression.rs"]
1501mod cohort_activation_regression;