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 camel_api::error_handler::ErrorHandlerConfig;
16use camel_api::metrics::MetricsCollector;
17#[allow(unused_imports)]
18use camel_api::{
19 BoxProcessor, CamelError, Exchange, FunctionInvoker, IdentityProcessor, NoOpMetrics,
20 NoopPlatformService, PlatformService, ProducerContext, RouteController, RuntimeHandle,
21 StepLifecycle,
22};
23use camel_component_api::{Consumer, ConsumerContext, consumer::ExchangeEnvelope};
24use camel_processor::aggregator::AggregatorService;
25pub use camel_processor::aggregator::SharedLanguageRegistry;
26
27use crate::health_registry::HealthCheckRegistry;
28use crate::lifecycle::adapters::controller_component_context::ControllerComponentContext;
29use crate::lifecycle::adapters::route_compiler_ext::{RouteCompilerExt, build_eh_config_pipeline};
30use crate::lifecycle::adapters::route_helpers::{
31 AggregateSplitInfo, CrashNotification, ManagedRoute, assert_no_mixed_top_level_splits,
32 handle_is_running, inferred_lifecycle_label, is_pending,
33};
34#[cfg(test)]
35pub(super) use crate::lifecycle::adapters::route_helpers::{
36 emit_start_route_event, set_start_route_event_hook,
37};
38use crate::lifecycle::adapters::route_registry::RouteRegistry;
39use crate::lifecycle::adapters::route_runtime_state;
40use crate::lifecycle::adapters::step_compilers::CompiledStep;
41use crate::lifecycle::application::route_definition::{BuilderStep, RouteDefinition};
42pub(crate) use crate::lifecycle::domain::CompiledPipeline;
43use crate::shared::components::domain::Registry;
44use crate::shared::observability::domain::{DetailLevel, TracerConfig};
45use camel_bean::BeanRegistry;
46
47pub struct DefaultRouteController {
55 pub(super) routes: RouteRegistry,
57 pub(super) registry: Arc<std::sync::Mutex<Registry>>,
59 pub(super) languages: SharedLanguageRegistry,
61 pub(super) beans: Arc<std::sync::Mutex<BeanRegistry>>,
63 pub(super) runtime: Option<Weak<dyn RuntimeHandle>>,
65 pub(super) global_error_handler: Option<ErrorHandlerConfig>,
67 pub(super) crash_notifier: Option<mpsc::Sender<CrashNotification>>,
69 pub(super) tracing_enabled: bool,
71 pub(super) tracer_detail_level: DetailLevel,
73 pub(super) tracer_metrics: Option<Arc<dyn MetricsCollector>>,
75 pub(super) platform_service: Arc<dyn PlatformService>,
76 pub(super) function_invoker: Option<Arc<dyn FunctionInvoker>>,
77 pub(super) health_registry: Option<Arc<HealthCheckRegistry>>,
78 pub(super) idempotent_repositories: crate::SharedIdempotentRegistry,
82 pub(super) claim_check_repositories: crate::SharedClaimCheckRegistry,
83 pub(super) cache_repositories: crate::SharedCacheRegistry,
84 pub(super) prepared_staging: HashMap<String, ManagedRoute>,
89 pub(super) endpoint_index: super::endpoint_index::EndpointIndex,
91}
92
93impl DefaultRouteController {
94 pub(super) fn health_registry(&self) -> Arc<HealthCheckRegistry> {
95 self.health_registry.clone().unwrap_or_else(|| {
96 debug!("health_registry not configured — creating isolated fallback");
97 Arc::new(HealthCheckRegistry::new(Duration::from_secs(5)))
98 })
99 }
100
101 pub fn new(
103 registry: Arc<std::sync::Mutex<Registry>>,
104 platform_service: Arc<dyn PlatformService>,
105 ) -> Self {
106 Self::with_beans_and_platform_service(
107 registry,
108 Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
109 platform_service,
110 )
111 }
112
113 pub fn with_beans(
115 registry: Arc<std::sync::Mutex<Registry>>,
116 beans: Arc<std::sync::Mutex<BeanRegistry>>,
117 ) -> Self {
118 Self::with_beans_and_platform_service(
119 registry,
120 beans,
121 Arc::new(NoopPlatformService::default()),
122 )
123 }
124
125 fn with_beans_and_platform_service(
126 registry: Arc<std::sync::Mutex<Registry>>,
127 beans: Arc<std::sync::Mutex<BeanRegistry>>,
128 platform_service: Arc<dyn PlatformService>,
129 ) -> Self {
130 Self {
131 routes: RouteRegistry::new(),
132 registry,
133 languages: Arc::new(std::sync::Mutex::new(HashMap::new())),
134 beans,
135 runtime: None,
136 global_error_handler: None,
137 crash_notifier: None,
138 tracing_enabled: false,
139 tracer_detail_level: DetailLevel::Minimal,
140 tracer_metrics: None,
141 platform_service,
142 function_invoker: None,
143 health_registry: None,
144 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
145 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
146 cache_repositories: Arc::new(crate::CacheRegistry::new()),
147 prepared_staging: HashMap::new(),
148 endpoint_index: super::endpoint_index::EndpointIndex::new(),
149 }
150 }
151
152 pub fn with_languages(
154 registry: Arc<std::sync::Mutex<Registry>>,
155 languages: SharedLanguageRegistry,
156 platform_service: Arc<dyn PlatformService>,
157 ) -> Self {
158 Self {
159 routes: RouteRegistry::new(),
160 registry,
161 languages,
162 beans: Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
163 runtime: None,
164 global_error_handler: None,
165 crash_notifier: None,
166 tracing_enabled: false,
167 tracer_detail_level: DetailLevel::Minimal,
168 tracer_metrics: None,
169 platform_service,
170 function_invoker: None,
171 health_registry: None,
172 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
173 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
174 cache_repositories: Arc::new(crate::CacheRegistry::new()),
175 prepared_staging: HashMap::new(),
176 endpoint_index: super::endpoint_index::EndpointIndex::new(),
177 }
178 }
179
180 pub fn with_languages_and_beans(
181 registry: Arc<std::sync::Mutex<Registry>>,
182 languages: SharedLanguageRegistry,
183 platform_service: Arc<dyn PlatformService>,
184 beans: Arc<std::sync::Mutex<BeanRegistry>>,
185 ) -> Self {
186 Self {
187 routes: RouteRegistry::new(),
188 registry,
189 languages,
190 beans,
191 runtime: None,
192 global_error_handler: None,
193 crash_notifier: None,
194 tracing_enabled: false,
195 tracer_detail_level: DetailLevel::Minimal,
196 tracer_metrics: None,
197 platform_service,
198 function_invoker: None,
199 health_registry: None,
200 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
201 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
202 cache_repositories: Arc::new(crate::CacheRegistry::new()),
203 prepared_staging: HashMap::new(),
204 endpoint_index: super::endpoint_index::EndpointIndex::new(),
205 }
206 }
207
208 pub fn with_function_invoker(mut self, function_invoker: Arc<dyn FunctionInvoker>) -> Self {
209 self.function_invoker = Some(function_invoker);
210 self
211 }
212
213 pub(crate) fn set_idempotent_repositories(
214 &mut self,
215 repositories: crate::SharedIdempotentRegistry,
216 ) {
217 self.idempotent_repositories = repositories;
218 }
219
220 pub(crate) fn set_claim_check_repositories(
221 &mut self,
222 repositories: crate::SharedClaimCheckRegistry,
223 ) {
224 self.claim_check_repositories = repositories;
225 }
226
227 pub(crate) fn set_cache_repositories(&mut self, repositories: crate::SharedCacheRegistry) {
228 self.cache_repositories = repositories;
229 }
230
231 pub fn set_health_registry(&mut self, registry: Arc<HealthCheckRegistry>) {
232 self.health_registry = Some(registry);
233 }
234
235 pub fn set_function_invoker(&mut self, invoker: Arc<dyn FunctionInvoker>) {
236 self.function_invoker = Some(invoker);
237 }
238
239 pub fn set_runtime_handle(&mut self, runtime: Arc<dyn RuntimeHandle>) {
241 self.runtime = Some(Arc::downgrade(&runtime));
242 }
243
244 pub fn set_crash_notifier(&mut self, tx: mpsc::Sender<CrashNotification>) {
249 self.crash_notifier = Some(tx);
250 }
251
252 pub fn set_error_handler(&mut self, config: ErrorHandlerConfig) {
254 self.global_error_handler = Some(config);
255 }
256
257 pub fn set_tracer_config(&mut self, config: &TracerConfig) {
259 self.tracing_enabled = config.enabled;
260 self.tracer_detail_level = config.detail_level.clone();
261 self.tracer_metrics = config.metrics_collector.clone();
262 }
263
264 fn build_producer_context(&self, route_id: &str) -> Result<ProducerContext, CamelError> {
265 let mut producer_ctx = ProducerContext::new().with_route_id(route_id);
266 if let Some(runtime) = self.runtime.as_ref().and_then(Weak::upgrade) {
267 producer_ctx = producer_ctx.with_runtime(runtime);
268 }
269 Ok(producer_ctx)
270 }
271
272 fn route_compiler_ext(&self) -> RouteCompilerExt<'_> {
274 RouteCompilerExt {
275 registry: &self.registry,
276 languages: &self.languages,
277 beans: &self.beans,
278 function_invoker: &self.function_invoker,
279 tracing_enabled: self.tracing_enabled,
280 tracer_detail_level: &self.tracer_detail_level,
281 tracer_metrics: &self.tracer_metrics,
282 platform_service: &self.platform_service,
283 runtime: &self.runtime,
284 global_error_handler: &self.global_error_handler,
285 health_registry: &self.health_registry,
286 route_registry: &self.routes,
287 idempotent_repositories: Arc::clone(&self.idempotent_repositories),
288 claim_check_repositories: Arc::clone(&self.claim_check_repositories),
289 cache_repositories: Arc::clone(&self.cache_repositories),
290 }
291 }
292
293 #[allow(dead_code)] pub(crate) fn resolve_steps(
296 &self,
297 steps: Vec<BuilderStep>,
298 producer_ctx: &ProducerContext,
299 registry: &Arc<std::sync::Mutex<Registry>>,
300 route_id: Option<&str>,
301 staging_mode: &super::step_resolution::FunctionStagingMode,
302 ) -> Result<Vec<CompiledStep>, CamelError> {
303 let component_ctx = Arc::new(ControllerComponentContext::new(
304 Arc::clone(registry),
305 Arc::clone(&self.languages),
306 self.tracer_metrics
307 .clone()
308 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
309 Arc::clone(&self.platform_service),
310 self.health_registry(),
311 route_id.map(|s| s.to_string()),
312 ));
313 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
314 Arc::clone(&component_ctx) as Arc<_>;
315
316 super::step_resolution::resolve_steps(
317 steps,
318 producer_ctx,
319 rt,
320 registry,
321 &self.languages,
322 &self.beans,
323 self.function_invoker.clone(),
324 component_ctx,
325 route_id,
326 staging_mode,
327 &self.idempotent_repositories,
328 &self.claim_check_repositories,
329 &self.cache_repositories,
330 )
331 }
332
333 pub async fn add_route(&mut self, definition: RouteDefinition) -> Result<(), CamelError> {
343 let route_id = definition.route_id().to_string();
344 let from_uri = definition.from_uri().to_string();
345
346 if self.routes.contains_key(&route_id) {
347 return Err(CamelError::RouteError(format!(
348 "duplicate route ID '{route_id}'"
349 )));
350 }
351
352 debug!(route_id = %route_id, "Adding route to controller");
353
354 let managed = match self.build_managed_route(
355 definition,
356 &super::step_resolution::FunctionStagingMode::DirectAdd,
357 ) {
358 Ok(managed) => managed,
359 Err(err) => {
360 self.discard_function_staging();
361 return Err(err);
362 }
363 };
364
365 if let Some(invoker) = &self.function_invoker
366 && let Err(err) = invoker.commit_staged().await
367 {
368 invoker.discard_staging(0);
369 return Err(CamelError::Config(err.to_string()));
370 }
371
372 self.routes
373 .insert(managed.definition.route_id().to_string(), managed);
374
375 self.endpoint_index.insert(&from_uri, &route_id);
376 Ok(())
377 }
378
379 pub(super) fn build_managed_route(
380 &self,
381 definition: RouteDefinition,
382 staging_mode: &super::step_resolution::FunctionStagingMode,
383 ) -> Result<ManagedRoute, CamelError> {
384 let route_id = definition.route_id().to_string();
385
386 let definition_info = definition.to_info();
387 let RouteDefinition {
388 from_uri,
389 steps,
390 error_handler,
391 circuit_breaker,
392 circuit_breaker_fallback,
393 security_policy,
394 security_authenticator,
395 unit_of_work,
396 concurrency,
397 ..
398 } = definition;
399
400 let producer_ctx = self.build_producer_context(&route_id)?;
401
402 assert_no_mixed_top_level_splits(&steps)?;
404
405 let (aggregate_split, processors_with_contracts) = self
406 .route_compiler_ext()
407 .detect_and_validate_route_split(steps, &producer_ctx, &route_id, staging_mode)?;
408 let mut lifecycle = super::route_helpers::collect_lifecycle(&processors_with_contracts);
409
410 let (circuit_breaker, fallback_lifecycle) = self.route_compiler_ext().attach_cb_fallback(
414 circuit_breaker,
415 circuit_breaker_fallback,
416 &producer_ctx,
417 &route_id,
418 staging_mode,
419 )?;
420 lifecycle.extend(fallback_lifecycle);
421 let route_id_for_tracing = route_id.clone();
422 let eh_config = error_handler.or_else(|| self.global_error_handler.clone());
423
424 let mut pipeline = build_eh_config_pipeline(
425 eh_config.as_ref(),
426 Arc::clone(&self.registry),
427 Arc::clone(&self.languages),
428 self.tracer_metrics.clone(),
429 Arc::clone(&self.platform_service),
430 self.health_registry(),
431 &route_id_for_tracing,
432 &producer_ctx,
433 processors_with_contracts,
434 self.tracing_enabled,
435 self.tracer_detail_level.clone(),
436 security_policy.clone(),
437 circuit_breaker,
438 )?;
439
440 let uow_counter = if let Some(uow_config) = &unit_of_work {
441 let component_ctx = Arc::new(ControllerComponentContext::new(
442 Arc::clone(&self.registry),
443 Arc::clone(&self.languages),
444 self.tracer_metrics
445 .clone()
446 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
447 Arc::clone(&self.platform_service),
448 self.health_registry(),
449 Some(route_id.clone()),
450 ));
451 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
452 Arc::clone(&component_ctx) as Arc<_>;
453 let (uow_layer, counter) = super::route_compiler_ext::resolve_uow_layer(
454 uow_config,
455 &producer_ctx,
456 rt,
457 component_ctx.as_ref(),
458 None,
459 )?;
460 pipeline = BoxProcessor::new(uow_layer.layer(pipeline));
461 Some(counter)
462 } else {
463 None
464 };
465
466 Ok(ManagedRoute {
467 definition: definition_info,
468 from_uri,
469 pipeline: super::pipeline_runtime::new_shared_pipeline_with_lifecycle(
470 pipeline, lifecycle,
471 ),
472 concurrency,
473 consumer_handle: None,
474 pipeline_handle: None,
475 consumer_cancel_token: CancellationToken::new(),
476 pipeline_cancel_token: CancellationToken::new(),
477 channel_sender: None,
478 in_flight: uow_counter,
479 drain_in_flight: Arc::new(std::sync::atomic::AtomicU64::new(0)),
480 aggregate_split,
481 agg_service: None,
482 compiled: route_runtime_state::CompiledRoute {
483 security_policy,
484 security_authenticator,
485 },
486 })
487 }
488
489 pub async fn add_route_with_generation(
490 &mut self,
491 definition: RouteDefinition,
492 generation: u64,
493 ) -> Result<(), CamelError> {
494 let route_id = definition.route_id().to_string();
495 let from_uri = definition.from_uri().to_string();
496
497 if self.routes.contains_key(&route_id) {
498 return Err(CamelError::RouteError(format!(
499 "duplicate route ID '{route_id}'"
500 )));
501 }
502
503 debug!(route_id = %route_id, generation, "Adding route to controller with generation");
504
505 let managed = self.build_managed_route(
506 definition,
507 &super::step_resolution::FunctionStagingMode::HotReload { generation },
508 )?;
509
510 self.routes.insert(route_id.clone(), managed);
511
512 self.endpoint_index.insert(&from_uri, &route_id);
513 Ok(())
514 }
515
516 pub async fn remove_route_preserving_functions(
517 &mut self,
518 route_id: &str,
519 ) -> Result<(), CamelError> {
520 let managed = self.routes.get(route_id).ok_or_else(|| {
521 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
522 })?;
523 if handle_is_running(&managed.consumer_handle)
524 || handle_is_running(&managed.pipeline_handle)
525 {
526 return Err(CamelError::RouteError(format!(
527 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
528 route_id,
529 inferred_lifecycle_label(managed)
530 )));
531 }
532 self.routes.remove(route_id);
533 if let Some(reg) = &self.health_registry {
534 reg.unregister_for_route(route_id);
535 }
536 self.endpoint_index.remove(route_id);
537 debug!(route_id = %route_id, "Route removed from controller (functions preserved for reload finalize)");
538 Ok(())
539 }
540
541 pub fn compile_route_definition(
544 &self,
545 def: RouteDefinition,
546 ) -> Result<BoxProcessor, CamelError> {
547 self.route_compiler_ext().compile_route_definition(def)
548 }
549
550 pub fn compile_route_definition_with_generation(
552 &self,
553 def: RouteDefinition,
554 generation: u64,
555 ) -> Result<BoxProcessor, CamelError> {
556 self.route_compiler_ext()
557 .compile_route_definition_with_generation(def, generation)
558 }
559
560 pub(crate) fn compile_route_definition_pipeline(
565 &self,
566 def: RouteDefinition,
567 generation: u64,
568 ) -> Result<CompiledPipeline, CamelError> {
569 self.route_compiler_ext()
570 .compile_route_definition_pipeline(def, generation)
571 }
572
573 pub(crate) fn compile_route_definition_dry_pipeline(
578 &self,
579 def: RouteDefinition,
580 ) -> Result<CompiledPipeline, CamelError> {
581 self.route_compiler_ext()
582 .compile_route_definition_dry_pipeline(def)
583 }
584
585 pub async fn remove_route(&mut self, route_id: &str) -> Result<(), CamelError> {
591 let managed = self.routes.get(route_id).ok_or_else(|| {
592 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
593 })?;
594 if handle_is_running(&managed.consumer_handle)
595 || handle_is_running(&managed.pipeline_handle)
596 {
597 return Err(CamelError::RouteError(format!(
598 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
599 route_id,
600 inferred_lifecycle_label(managed)
601 )));
602 }
603 if let Some(invoker) = &self.function_invoker {
604 for (id, rid) in self.collect_function_refs(route_id) {
605 if let Err(e) = invoker.unregister(&id, rid.as_deref()).await {
606 warn!(route_id = %route_id, error = %e, "Failed to unregister function during route removal");
607 }
608 }
609 }
610 self.routes.remove(route_id);
611 if let Some(reg) = &self.health_registry {
612 reg.unregister_for_route(route_id);
613 }
614 self.endpoint_index.remove(route_id);
615 info!(route_id = %route_id, "Route removed from controller");
616 Ok(())
617 }
618
619 fn collect_function_refs(
620 &self,
621 route_id: &str,
622 ) -> Vec<(camel_api::FunctionId, Option<String>)> {
623 self.function_invoker
624 .as_ref()
625 .map(|invoker| invoker.function_refs_for_route(route_id))
626 .unwrap_or_default()
627 }
628
629 fn discard_function_staging(&self) {
630 if let Some(invoker) = &self.function_invoker {
631 invoker.discard_staging(0);
632 }
633 }
634
635 pub fn route_count(&self) -> usize {
637 self.routes.route_count()
638 }
639
640 pub fn in_flight_count(&self, route_id: &str) -> Option<u64> {
641 self.routes.in_flight_count(route_id)
642 }
643
644 pub fn route_exists(&self, route_id: &str) -> bool {
646 self.routes.route_exists(route_id)
647 }
648
649 pub fn route_ids(&self) -> Vec<String> {
651 self.routes.route_ids()
652 }
653
654 pub fn route_source_hash(&self, route_id: &str) -> Option<u64> {
655 self.routes.route_source_hash(route_id)
656 }
657
658 pub fn auto_startup_route_ids(&self) -> Vec<String> {
660 self.routes.auto_startup_route_ids()
661 }
662
663 pub fn shutdown_route_ids(&self) -> Vec<String> {
665 self.routes.shutdown_route_ids()
666 }
667
668 pub fn swap_pipeline(
687 &self,
688 route_id: &str,
689 new_pipeline: BoxProcessor,
690 ) -> Result<(), CamelError> {
691 let managed = self
692 .routes
693 .get(route_id)
694 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
695
696 let assembly = managed.pipeline.load();
697 let has_lifecycle = !assembly.lifecycle.is_empty();
698
699 if has_lifecycle || managed.agg_service.is_some() {
700 warn!(
701 route_id = %route_id,
702 "Hot-swap rejected — route has lifecycle/agg steps; use Restart path"
703 );
704 return Err(CamelError::RouteError(format!(
705 "Route '{}' contains stateful steps (lifecycle-bearing). Hot-swap not supported — use restart.",
706 route_id
707 )));
708 }
709
710 drop(assembly);
711
712 if managed.aggregate_split.is_some() {
713 warn!(
714 route_id = %route_id,
715 "swap_pipeline: aggregate routes with timeout do not support hot-reload of pre/post segments"
716 );
717 }
718
719 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, vec![]);
720 debug!(route_id = %route_id, "Pipeline swapped atomically");
721 Ok(())
722 }
723
724 pub(crate) fn swap_pipeline_raw(
734 &self,
735 route_id: &str,
736 new_pipeline: BoxProcessor,
737 lifecycle: Vec<Arc<dyn StepLifecycle>>,
738 ) -> Result<(), CamelError> {
739 let managed = self
740 .routes
741 .get(route_id)
742 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
743 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, lifecycle);
744 debug!(route_id = %route_id, "Pipeline swapped (raw — lifecycle bypass)");
745 Ok(())
746 }
747
748 pub fn route_from_uri(&self, route_id: &str) -> Option<String> {
750 self.routes.route_from_uri(route_id)
751 }
752
753 pub fn routes_for_endpoint(&self, uri: &str) -> Vec<String> {
755 self.endpoint_index.routes_for(uri)
756 }
757
758 pub fn list_endpoint_uris(&self) -> Vec<String> {
760 self.endpoint_index.list_uris()
761 }
762
763 pub fn get_pipeline(&self, route_id: &str) -> Option<BoxProcessor> {
768 self.routes.get_pipeline(route_id)
769 }
770
771 pub(crate) fn route_has_lifecycle(&self, route_id: &str) -> bool {
775 self.routes
776 .get(route_id)
777 .map(|managed| !managed.pipeline.load().lifecycle.is_empty())
778 .unwrap_or(false)
779 }
780
781 pub(super) async fn stop_route_internal(&mut self, route_id: &str) -> Result<(), CamelError> {
783 self.routes.stop_route(route_id).await
784 }
785
786 pub async fn start_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
787 self.start_route(route_id).await
788 }
789
790 pub async fn stop_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
791 self.stop_route(route_id).await
792 }
793}
794
795impl DefaultRouteController {
798 #[allow(clippy::too_many_arguments)]
804 pub(super) async fn start_aggregate_route(
805 &mut self,
806 route_id: &str,
807 split: AggregateSplitInfo,
808 consumer: Box<dyn Consumer>,
809 consumer_ctx: ConsumerContext,
810 mut rx: mpsc::Receiver<ExchangeEnvelope>,
811 crash_notifier: Option<mpsc::Sender<CrashNotification>>,
812 runtime_for_consumer: Option<Weak<dyn RuntimeHandle>>,
813 tx_for_storage: mpsc::Sender<ExchangeEnvelope>,
814 pipeline_cancel: CancellationToken,
816 drain_in_flight: Arc<std::sync::atomic::AtomicU64>,
817 ) -> Result<(), CamelError> {
818 let (late_tx, late_rx) = mpsc::channel::<Exchange>(256);
819
820 let route_cancel_clone = pipeline_cancel.clone();
821 let svc = AggregatorService::new(
822 split.agg_config.clone(),
823 late_tx,
824 Arc::clone(&self.languages),
825 route_cancel_clone,
826 );
827 let agg = Arc::new(svc);
828
829 let pipeline_cancel_for_monitor = pipeline_cancel.clone();
830 let agg_for_monitor = Arc::clone(&agg);
831
832 {
833 let managed = self
834 .routes
835 .get_mut(route_id)
836 .expect("invariant: route must exist"); managed.agg_service = Some(Arc::clone(&agg));
838 }
839
840 let late_rx = Arc::new(tokio::sync::Mutex::new(late_rx));
841 let pre_pipeline = split.pre_pipeline;
842 let post_pipeline = split.post_pipeline;
843
844 let pipeline_handle = tokio::spawn(async move {
846 loop {
847 tokio::select! {
848 biased;
849
850 late_ex = async {
851 let mut rx = late_rx.lock().await;
852 rx.recv().await
853 } => {
854 match late_ex {
855 Some(ex) => {
856 let pipe = post_pipeline.load();
857 if let Err(e) = pipe.processor.clone_inner().oneshot(ex).await {
858 tracing::warn!(error = %e, "late exchange post-pipeline failed");
859 }
860 }
861 None => return,
862 }
863 }
864
865 envelope_opt = rx.recv() => {
866 match envelope_opt {
867 Some(envelope) => {
868 let ExchangeEnvelope { exchange, reply_tx } = envelope;
869 let _drain_guard = super::route_helpers::DrainGuard::new(Arc::clone(&drain_in_flight));
870 let pre_pipe = pre_pipeline.load();
871 let ex = match pre_pipe.processor.clone_inner().oneshot(exchange).await {
872 Ok(ex) => ex,
873 Err(e) => {
874 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
875 continue;
876 }
877 };
878
879 let ex = {
880 let cloned_svc = agg.as_ref().clone();
881 cloned_svc.oneshot(ex).await
882 };
883
884 match ex {
885 Ok(ex) => {
886 if !is_pending(&ex) {
887 let post_pipe = post_pipeline.load();
888 let out = post_pipe.processor.clone_inner().oneshot(ex).await;
889 if let Some(tx) = reply_tx { let _ = tx.send(out); }
890 } else if let Some(tx) = reply_tx {
891 let _ = tx.send(Ok(ex));
892 }
893 }
894 Err(e) => {
895 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
896 }
897 }
898 }
899 None => return,
900 }
901 }
902
903 _ = pipeline_cancel.cancelled() => {
904 agg.force_complete_all();
905 let mut rx_guard = late_rx.lock().await;
906 while let Ok(late_ex) = rx_guard.try_recv() {
907 let pipe = post_pipeline.load();
908 let _ = pipe.processor.clone_inner().oneshot(late_ex).await;
909 }
910 break;
911 }
912 }
913 }
914 });
915 #[cfg(test)]
916 emit_start_route_event("pipeline_spawned");
917
918 let (consumer_handle, startup_rx) = super::consumer_management::spawn_consumer_task(
921 route_id.to_string(),
922 consumer,
923 consumer_ctx,
924 crash_notifier,
925 runtime_for_consumer,
926 false,
927 );
928
929 if let Err(e) =
936 super::consumer_management::await_consumer_startup(startup_rx, "startup").await
937 {
938 consumer_handle.abort();
939 pipeline_cancel_for_monitor.cancel();
940 return Err(e);
941 }
942
943 let force_on_stop = agg_for_monitor.config().force_completion_on_stop;
947 let consumer_handle = tokio::spawn(async move {
948 let _ = consumer_handle.await;
949 if !pipeline_cancel_for_monitor.is_cancelled() {
950 agg_for_monitor.force_complete_all();
951 if force_on_stop {
952 pipeline_cancel_for_monitor.cancel();
953 }
954 }
955 });
956 #[cfg(test)]
957 emit_start_route_event("consumer_spawned");
958
959 {
960 let managed = self
961 .routes
962 .get_mut(route_id)
963 .expect("invariant: route must exist"); managed.consumer_handle = Some(consumer_handle);
965 managed.pipeline_handle = Some(pipeline_handle);
966 managed.channel_sender = Some(tx_for_storage);
967 }
968
969 info!(route_id = %route_id, "Route started (aggregate with timeout)");
970 Ok(())
971 }
972
973 #[cfg(test)]
978 pub(crate) fn set_route_lifecycle_for_test(
979 &mut self,
980 route_id: &str,
981 lifecycle: Vec<Arc<dyn StepLifecycle>>,
982 ) -> Result<(), CamelError> {
983 use super::pipeline_runtime::PipelineAssembly;
984 use camel_api::SyncBoxProcessor;
985 use std::sync::Arc;
986
987 let managed = self
988 .routes
989 .get_mut(route_id)
990 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
991 let old_processor = managed.pipeline.load().processor.clone_inner();
992 managed.pipeline.store(Arc::new(PipelineAssembly::new(
993 SyncBoxProcessor::new(old_processor),
994 lifecycle,
995 )));
996 Ok(())
997 }
998}
999
1000#[cfg(test)]
1001impl crate::hot_reload::ports::ReloadIntrospectionPort for DefaultRouteController {
1002 fn route_ids(&self) -> Vec<String> {
1003 self.route_ids() }
1005 fn route_from_uri(&self, route_id: &str) -> Option<String> {
1006 self.route_from_uri(route_id) }
1008 fn route_source_hash(&self, route_id: &str) -> Option<u64> {
1009 self.route_source_hash(route_id) }
1011}
1012
1013#[cfg(test)]
1014#[path = "route_controller_tests.rs"]
1015mod tests;