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) prepared_staging: HashMap<String, ManagedRoute>,
88 pub(super) endpoint_index: super::endpoint_index::EndpointIndex,
90}
91
92impl DefaultRouteController {
93 pub(super) fn health_registry(&self) -> Arc<HealthCheckRegistry> {
94 self.health_registry.clone().unwrap_or_else(|| {
95 debug!("health_registry not configured — creating isolated fallback");
96 Arc::new(HealthCheckRegistry::new(Duration::from_secs(5)))
97 })
98 }
99
100 pub fn new(
102 registry: Arc<std::sync::Mutex<Registry>>,
103 platform_service: Arc<dyn PlatformService>,
104 ) -> Self {
105 Self::with_beans_and_platform_service(
106 registry,
107 Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
108 platform_service,
109 )
110 }
111
112 pub fn with_beans(
114 registry: Arc<std::sync::Mutex<Registry>>,
115 beans: Arc<std::sync::Mutex<BeanRegistry>>,
116 ) -> Self {
117 Self::with_beans_and_platform_service(
118 registry,
119 beans,
120 Arc::new(NoopPlatformService::default()),
121 )
122 }
123
124 fn with_beans_and_platform_service(
125 registry: Arc<std::sync::Mutex<Registry>>,
126 beans: Arc<std::sync::Mutex<BeanRegistry>>,
127 platform_service: Arc<dyn PlatformService>,
128 ) -> Self {
129 Self {
130 routes: RouteRegistry::new(),
131 registry,
132 languages: Arc::new(std::sync::Mutex::new(HashMap::new())),
133 beans,
134 runtime: None,
135 global_error_handler: None,
136 crash_notifier: None,
137 tracing_enabled: false,
138 tracer_detail_level: DetailLevel::Minimal,
139 tracer_metrics: None,
140 platform_service,
141 function_invoker: None,
142 health_registry: None,
143 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
144 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
145 prepared_staging: HashMap::new(),
146 endpoint_index: super::endpoint_index::EndpointIndex::new(),
147 }
148 }
149
150 pub fn with_languages(
152 registry: Arc<std::sync::Mutex<Registry>>,
153 languages: SharedLanguageRegistry,
154 platform_service: Arc<dyn PlatformService>,
155 ) -> Self {
156 Self {
157 routes: RouteRegistry::new(),
158 registry,
159 languages,
160 beans: Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
161 runtime: None,
162 global_error_handler: None,
163 crash_notifier: None,
164 tracing_enabled: false,
165 tracer_detail_level: DetailLevel::Minimal,
166 tracer_metrics: None,
167 platform_service,
168 function_invoker: None,
169 health_registry: None,
170 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
171 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
172 prepared_staging: HashMap::new(),
173 endpoint_index: super::endpoint_index::EndpointIndex::new(),
174 }
175 }
176
177 pub fn with_languages_and_beans(
178 registry: Arc<std::sync::Mutex<Registry>>,
179 languages: SharedLanguageRegistry,
180 platform_service: Arc<dyn PlatformService>,
181 beans: Arc<std::sync::Mutex<BeanRegistry>>,
182 ) -> Self {
183 Self {
184 routes: RouteRegistry::new(),
185 registry,
186 languages,
187 beans,
188 runtime: None,
189 global_error_handler: None,
190 crash_notifier: None,
191 tracing_enabled: false,
192 tracer_detail_level: DetailLevel::Minimal,
193 tracer_metrics: None,
194 platform_service,
195 function_invoker: None,
196 health_registry: None,
197 idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
198 claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
199 prepared_staging: HashMap::new(),
200 endpoint_index: super::endpoint_index::EndpointIndex::new(),
201 }
202 }
203
204 pub fn with_function_invoker(mut self, function_invoker: Arc<dyn FunctionInvoker>) -> Self {
205 self.function_invoker = Some(function_invoker);
206 self
207 }
208
209 pub(crate) fn set_idempotent_repositories(
210 &mut self,
211 repositories: crate::SharedIdempotentRegistry,
212 ) {
213 self.idempotent_repositories = repositories;
214 }
215
216 pub(crate) fn set_claim_check_repositories(
217 &mut self,
218 repositories: crate::SharedClaimCheckRegistry,
219 ) {
220 self.claim_check_repositories = repositories;
221 }
222
223 pub fn set_health_registry(&mut self, registry: Arc<HealthCheckRegistry>) {
224 self.health_registry = Some(registry);
225 }
226
227 pub fn set_function_invoker(&mut self, invoker: Arc<dyn FunctionInvoker>) {
228 self.function_invoker = Some(invoker);
229 }
230
231 pub fn set_runtime_handle(&mut self, runtime: Arc<dyn RuntimeHandle>) {
233 self.runtime = Some(Arc::downgrade(&runtime));
234 }
235
236 pub fn set_crash_notifier(&mut self, tx: mpsc::Sender<CrashNotification>) {
241 self.crash_notifier = Some(tx);
242 }
243
244 pub fn set_error_handler(&mut self, config: ErrorHandlerConfig) {
246 self.global_error_handler = Some(config);
247 }
248
249 pub fn set_tracer_config(&mut self, config: &TracerConfig) {
251 self.tracing_enabled = config.enabled;
252 self.tracer_detail_level = config.detail_level.clone();
253 self.tracer_metrics = config.metrics_collector.clone();
254 }
255
256 fn build_producer_context(&self, route_id: &str) -> Result<ProducerContext, CamelError> {
257 let mut producer_ctx = ProducerContext::new().with_route_id(route_id);
258 if let Some(runtime) = self.runtime.as_ref().and_then(Weak::upgrade) {
259 producer_ctx = producer_ctx.with_runtime(runtime);
260 }
261 Ok(producer_ctx)
262 }
263
264 fn route_compiler_ext(&self) -> RouteCompilerExt<'_> {
266 RouteCompilerExt {
267 registry: &self.registry,
268 languages: &self.languages,
269 beans: &self.beans,
270 function_invoker: &self.function_invoker,
271 tracing_enabled: self.tracing_enabled,
272 tracer_detail_level: &self.tracer_detail_level,
273 tracer_metrics: &self.tracer_metrics,
274 platform_service: &self.platform_service,
275 runtime: &self.runtime,
276 global_error_handler: &self.global_error_handler,
277 health_registry: &self.health_registry,
278 route_registry: &self.routes,
279 idempotent_repositories: Arc::clone(&self.idempotent_repositories),
280 claim_check_repositories: Arc::clone(&self.claim_check_repositories),
281 }
282 }
283
284 #[allow(dead_code)] pub(crate) fn resolve_steps(
287 &self,
288 steps: Vec<BuilderStep>,
289 producer_ctx: &ProducerContext,
290 registry: &Arc<std::sync::Mutex<Registry>>,
291 route_id: Option<&str>,
292 staging_mode: &super::step_resolution::FunctionStagingMode,
293 ) -> Result<Vec<CompiledStep>, CamelError> {
294 let component_ctx = Arc::new(ControllerComponentContext::new(
295 Arc::clone(registry),
296 Arc::clone(&self.languages),
297 self.tracer_metrics
298 .clone()
299 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
300 Arc::clone(&self.platform_service),
301 self.health_registry(),
302 route_id.map(|s| s.to_string()),
303 ));
304 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
305 Arc::clone(&component_ctx) as Arc<_>;
306
307 super::step_resolution::resolve_steps(
308 steps,
309 producer_ctx,
310 rt,
311 registry,
312 &self.languages,
313 &self.beans,
314 self.function_invoker.clone(),
315 component_ctx,
316 route_id,
317 staging_mode,
318 &self.idempotent_repositories,
319 &self.claim_check_repositories,
320 )
321 }
322
323 pub async fn add_route(&mut self, definition: RouteDefinition) -> Result<(), CamelError> {
333 let route_id = definition.route_id().to_string();
334 let from_uri = definition.from_uri().to_string();
335
336 if self.routes.contains_key(&route_id) {
337 return Err(CamelError::RouteError(format!(
338 "Route '{}' already exists",
339 route_id
340 )));
341 }
342
343 debug!(route_id = %route_id, "Adding route to controller");
344
345 let managed = match self.build_managed_route(
346 definition,
347 &super::step_resolution::FunctionStagingMode::DirectAdd,
348 ) {
349 Ok(managed) => managed,
350 Err(err) => {
351 self.discard_function_staging();
352 return Err(err);
353 }
354 };
355
356 if let Some(invoker) = &self.function_invoker
357 && let Err(err) = invoker.commit_staged().await
358 {
359 invoker.discard_staging(0);
360 return Err(CamelError::Config(err.to_string()));
361 }
362
363 self.routes
364 .insert(managed.definition.route_id().to_string(), managed);
365
366 self.endpoint_index.insert(&from_uri, &route_id);
367 Ok(())
368 }
369
370 pub(super) fn build_managed_route(
371 &self,
372 definition: RouteDefinition,
373 staging_mode: &super::step_resolution::FunctionStagingMode,
374 ) -> Result<ManagedRoute, CamelError> {
375 let route_id = definition.route_id().to_string();
376
377 let definition_info = definition.to_info();
378 let RouteDefinition {
379 from_uri,
380 steps,
381 error_handler,
382 circuit_breaker,
383 security_policy,
384 security_authenticator,
385 unit_of_work,
386 concurrency,
387 ..
388 } = definition;
389
390 let producer_ctx = self.build_producer_context(&route_id)?;
391
392 assert_no_mixed_top_level_splits(&steps)?;
394
395 let (aggregate_split, processors_with_contracts) = self
396 .route_compiler_ext()
397 .detect_and_validate_route_split(steps, &producer_ctx, &route_id, staging_mode)?;
398 let lifecycle = super::route_helpers::collect_lifecycle(&processors_with_contracts);
399 let route_id_for_tracing = route_id.clone();
400 let eh_config = error_handler.or_else(|| self.global_error_handler.clone());
401
402 let mut pipeline = build_eh_config_pipeline(
403 eh_config.as_ref(),
404 Arc::clone(&self.registry),
405 Arc::clone(&self.languages),
406 self.tracer_metrics.clone(),
407 Arc::clone(&self.platform_service),
408 self.health_registry(),
409 &route_id_for_tracing,
410 &producer_ctx,
411 processors_with_contracts,
412 self.tracing_enabled,
413 self.tracer_detail_level.clone(),
414 security_policy.clone(),
415 circuit_breaker,
416 )?;
417
418 let uow_counter = if let Some(uow_config) = &unit_of_work {
419 let component_ctx = Arc::new(ControllerComponentContext::new(
420 Arc::clone(&self.registry),
421 Arc::clone(&self.languages),
422 self.tracer_metrics
423 .clone()
424 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
425 Arc::clone(&self.platform_service),
426 self.health_registry(),
427 Some(route_id.clone()),
428 ));
429 let rt: Arc<dyn camel_component_api::RuntimeObservability> =
430 Arc::clone(&component_ctx) as Arc<_>;
431 let (uow_layer, counter) = super::route_compiler_ext::resolve_uow_layer(
432 uow_config,
433 &producer_ctx,
434 rt,
435 component_ctx.as_ref(),
436 None,
437 )?;
438 pipeline = BoxProcessor::new(uow_layer.layer(pipeline));
439 Some(counter)
440 } else {
441 None
442 };
443
444 Ok(ManagedRoute {
445 definition: definition_info,
446 from_uri,
447 pipeline: super::pipeline_runtime::new_shared_pipeline_with_lifecycle(
448 pipeline, lifecycle,
449 ),
450 concurrency,
451 consumer_handle: None,
452 pipeline_handle: None,
453 consumer_cancel_token: CancellationToken::new(),
454 pipeline_cancel_token: CancellationToken::new(),
455 channel_sender: None,
456 in_flight: uow_counter,
457 drain_in_flight: Arc::new(std::sync::atomic::AtomicU64::new(0)),
458 aggregate_split,
459 agg_service: None,
460 compiled: route_runtime_state::CompiledRoute {
461 security_policy,
462 security_authenticator,
463 },
464 })
465 }
466
467 pub async fn add_route_with_generation(
468 &mut self,
469 definition: RouteDefinition,
470 generation: u64,
471 ) -> Result<(), CamelError> {
472 let route_id = definition.route_id().to_string();
473 let from_uri = definition.from_uri().to_string();
474
475 if self.routes.contains_key(&route_id) {
476 return Err(CamelError::RouteError(format!(
477 "Route '{}' already exists",
478 route_id
479 )));
480 }
481
482 debug!(route_id = %route_id, generation, "Adding route to controller with generation");
483
484 let managed = self.build_managed_route(
485 definition,
486 &super::step_resolution::FunctionStagingMode::HotReload { generation },
487 )?;
488
489 self.routes.insert(route_id.clone(), managed);
490
491 self.endpoint_index.insert(&from_uri, &route_id);
492 Ok(())
493 }
494
495 pub async fn remove_route_preserving_functions(
496 &mut self,
497 route_id: &str,
498 ) -> Result<(), CamelError> {
499 let managed = self.routes.get(route_id).ok_or_else(|| {
500 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
501 })?;
502 if handle_is_running(&managed.consumer_handle)
503 || handle_is_running(&managed.pipeline_handle)
504 {
505 return Err(CamelError::RouteError(format!(
506 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
507 route_id,
508 inferred_lifecycle_label(managed)
509 )));
510 }
511 self.routes.remove(route_id);
512 if let Some(reg) = &self.health_registry {
513 reg.unregister_for_route(route_id);
514 }
515 self.endpoint_index.remove(route_id);
516 debug!(route_id = %route_id, "Route removed from controller (functions preserved for reload finalize)");
517 Ok(())
518 }
519
520 pub fn compile_route_definition(
523 &self,
524 def: RouteDefinition,
525 ) -> Result<BoxProcessor, CamelError> {
526 self.route_compiler_ext().compile_route_definition(def)
527 }
528
529 pub fn compile_route_definition_with_generation(
531 &self,
532 def: RouteDefinition,
533 generation: u64,
534 ) -> Result<BoxProcessor, CamelError> {
535 self.route_compiler_ext()
536 .compile_route_definition_with_generation(def, generation)
537 }
538
539 pub(crate) fn compile_route_definition_pipeline(
544 &self,
545 def: RouteDefinition,
546 generation: u64,
547 ) -> Result<CompiledPipeline, CamelError> {
548 self.route_compiler_ext()
549 .compile_route_definition_pipeline(def, generation)
550 }
551
552 pub(crate) fn compile_route_definition_dry_pipeline(
557 &self,
558 def: RouteDefinition,
559 ) -> Result<CompiledPipeline, CamelError> {
560 self.route_compiler_ext()
561 .compile_route_definition_dry_pipeline(def)
562 }
563
564 pub async fn remove_route(&mut self, route_id: &str) -> Result<(), CamelError> {
570 let managed = self.routes.get(route_id).ok_or_else(|| {
571 CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
572 })?;
573 if handle_is_running(&managed.consumer_handle)
574 || handle_is_running(&managed.pipeline_handle)
575 {
576 return Err(CamelError::RouteError(format!(
577 "Route '{}' must be stopped before removal (current execution lifecycle: {})",
578 route_id,
579 inferred_lifecycle_label(managed)
580 )));
581 }
582 if let Some(invoker) = &self.function_invoker {
583 for (id, rid) in self.collect_function_refs(route_id) {
584 if let Err(e) = invoker.unregister(&id, rid.as_deref()).await {
585 warn!(route_id = %route_id, error = %e, "Failed to unregister function during route removal");
586 }
587 }
588 }
589 self.routes.remove(route_id);
590 if let Some(reg) = &self.health_registry {
591 reg.unregister_for_route(route_id);
592 }
593 self.endpoint_index.remove(route_id);
594 info!(route_id = %route_id, "Route removed from controller");
595 Ok(())
596 }
597
598 fn collect_function_refs(
599 &self,
600 route_id: &str,
601 ) -> Vec<(camel_api::FunctionId, Option<String>)> {
602 self.function_invoker
603 .as_ref()
604 .map(|invoker| invoker.function_refs_for_route(route_id))
605 .unwrap_or_default()
606 }
607
608 fn discard_function_staging(&self) {
609 if let Some(invoker) = &self.function_invoker {
610 invoker.discard_staging(0);
611 }
612 }
613
614 pub fn route_count(&self) -> usize {
616 self.routes.route_count()
617 }
618
619 pub fn in_flight_count(&self, route_id: &str) -> Option<u64> {
620 self.routes.in_flight_count(route_id)
621 }
622
623 pub fn route_exists(&self, route_id: &str) -> bool {
625 self.routes.route_exists(route_id)
626 }
627
628 pub fn route_ids(&self) -> Vec<String> {
630 self.routes.route_ids()
631 }
632
633 pub fn route_source_hash(&self, route_id: &str) -> Option<u64> {
634 self.routes.route_source_hash(route_id)
635 }
636
637 pub fn auto_startup_route_ids(&self) -> Vec<String> {
639 self.routes.auto_startup_route_ids()
640 }
641
642 pub fn shutdown_route_ids(&self) -> Vec<String> {
644 self.routes.shutdown_route_ids()
645 }
646
647 pub fn swap_pipeline(
666 &self,
667 route_id: &str,
668 new_pipeline: BoxProcessor,
669 ) -> Result<(), CamelError> {
670 let managed = self
671 .routes
672 .get(route_id)
673 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
674
675 let assembly = managed.pipeline.load();
676 let has_lifecycle = !assembly.lifecycle.is_empty();
677
678 if has_lifecycle || managed.agg_service.is_some() {
679 warn!(
680 route_id = %route_id,
681 "Hot-swap rejected — route has lifecycle/agg steps; use Restart path"
682 );
683 return Err(CamelError::RouteError(format!(
684 "Route '{}' contains stateful steps (lifecycle-bearing). Hot-swap not supported — use restart.",
685 route_id
686 )));
687 }
688
689 drop(assembly);
690
691 if managed.aggregate_split.is_some() {
692 warn!(
693 route_id = %route_id,
694 "swap_pipeline: aggregate routes with timeout do not support hot-reload of pre/post segments"
695 );
696 }
697
698 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, vec![]);
699 debug!(route_id = %route_id, "Pipeline swapped atomically");
700 Ok(())
701 }
702
703 pub(crate) fn swap_pipeline_raw(
713 &self,
714 route_id: &str,
715 new_pipeline: BoxProcessor,
716 lifecycle: Vec<Arc<dyn StepLifecycle>>,
717 ) -> Result<(), CamelError> {
718 let managed = self
719 .routes
720 .get(route_id)
721 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
722 super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, lifecycle);
723 debug!(route_id = %route_id, "Pipeline swapped (raw — lifecycle bypass)");
724 Ok(())
725 }
726
727 pub fn route_from_uri(&self, route_id: &str) -> Option<String> {
729 self.routes.route_from_uri(route_id)
730 }
731
732 pub fn routes_for_endpoint(&self, uri: &str) -> Vec<String> {
734 self.endpoint_index.routes_for(uri)
735 }
736
737 pub fn list_endpoint_uris(&self) -> Vec<String> {
739 self.endpoint_index.list_uris()
740 }
741
742 pub fn get_pipeline(&self, route_id: &str) -> Option<BoxProcessor> {
747 self.routes.get_pipeline(route_id)
748 }
749
750 pub(crate) fn route_has_lifecycle(&self, route_id: &str) -> bool {
754 self.routes
755 .get(route_id)
756 .map(|managed| !managed.pipeline.load().lifecycle.is_empty())
757 .unwrap_or(false)
758 }
759
760 pub(super) async fn stop_route_internal(&mut self, route_id: &str) -> Result<(), CamelError> {
762 self.routes.stop_route(route_id).await
763 }
764
765 pub async fn start_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
766 self.start_route(route_id).await
767 }
768
769 pub async fn stop_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
770 self.stop_route(route_id).await
771 }
772}
773
774impl DefaultRouteController {
777 #[allow(clippy::too_many_arguments)]
783 pub(super) async fn start_aggregate_route(
784 &mut self,
785 route_id: &str,
786 split: AggregateSplitInfo,
787 consumer: Box<dyn Consumer>,
788 consumer_ctx: ConsumerContext,
789 mut rx: mpsc::Receiver<ExchangeEnvelope>,
790 crash_notifier: Option<mpsc::Sender<CrashNotification>>,
791 runtime_for_consumer: Option<Weak<dyn RuntimeHandle>>,
792 tx_for_storage: mpsc::Sender<ExchangeEnvelope>,
793 pipeline_cancel: CancellationToken,
795 drain_in_flight: Arc<std::sync::atomic::AtomicU64>,
796 ) -> Result<(), CamelError> {
797 let (late_tx, late_rx) = mpsc::channel::<Exchange>(256);
798
799 let route_cancel_clone = pipeline_cancel.clone();
800 let svc = AggregatorService::new(
801 split.agg_config.clone(),
802 late_tx,
803 Arc::clone(&self.languages),
804 route_cancel_clone,
805 );
806 let agg = Arc::new(svc);
807
808 let pipeline_cancel_for_monitor = pipeline_cancel.clone();
809 let agg_for_monitor = Arc::clone(&agg);
810
811 {
812 let managed = self
813 .routes
814 .get_mut(route_id)
815 .expect("invariant: route must exist"); managed.agg_service = Some(Arc::clone(&agg));
817 }
818
819 let late_rx = Arc::new(tokio::sync::Mutex::new(late_rx));
820 let pre_pipeline = split.pre_pipeline;
821 let post_pipeline = split.post_pipeline;
822
823 let pipeline_handle = tokio::spawn(async move {
825 loop {
826 tokio::select! {
827 biased;
828
829 late_ex = async {
830 let mut rx = late_rx.lock().await;
831 rx.recv().await
832 } => {
833 match late_ex {
834 Some(ex) => {
835 let pipe = post_pipeline.load();
836 if let Err(e) = pipe.processor.clone_inner().oneshot(ex).await {
837 tracing::warn!(error = %e, "late exchange post-pipeline failed");
838 }
839 }
840 None => return,
841 }
842 }
843
844 envelope_opt = rx.recv() => {
845 match envelope_opt {
846 Some(envelope) => {
847 let ExchangeEnvelope { exchange, reply_tx } = envelope;
848 let _drain_guard = super::route_helpers::DrainGuard::new(Arc::clone(&drain_in_flight));
849 let pre_pipe = pre_pipeline.load();
850 let ex = match pre_pipe.processor.clone_inner().oneshot(exchange).await {
851 Ok(ex) => ex,
852 Err(e) => {
853 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
854 continue;
855 }
856 };
857
858 let ex = {
859 let cloned_svc = agg.as_ref().clone();
860 cloned_svc.oneshot(ex).await
861 };
862
863 match ex {
864 Ok(ex) => {
865 if !is_pending(&ex) {
866 let post_pipe = post_pipeline.load();
867 let out = post_pipe.processor.clone_inner().oneshot(ex).await;
868 if let Some(tx) = reply_tx { let _ = tx.send(out); }
869 } else if let Some(tx) = reply_tx {
870 let _ = tx.send(Ok(ex));
871 }
872 }
873 Err(e) => {
874 if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
875 }
876 }
877 }
878 None => return,
879 }
880 }
881
882 _ = pipeline_cancel.cancelled() => {
883 agg.force_complete_all();
884 let mut rx_guard = late_rx.lock().await;
885 while let Ok(late_ex) = rx_guard.try_recv() {
886 let pipe = post_pipeline.load();
887 let _ = pipe.processor.clone_inner().oneshot(late_ex).await;
888 }
889 break;
890 }
891 }
892 }
893 });
894 #[cfg(test)]
895 emit_start_route_event("pipeline_spawned");
896
897 let (consumer_handle, startup_rx) = super::consumer_management::spawn_consumer_task(
900 route_id.to_string(),
901 consumer,
902 consumer_ctx,
903 crash_notifier,
904 runtime_for_consumer,
905 false,
906 );
907
908 if let Err(e) =
915 super::consumer_management::await_consumer_startup(startup_rx, "startup").await
916 {
917 consumer_handle.abort();
918 pipeline_cancel_for_monitor.cancel();
919 return Err(e);
920 }
921
922 let force_on_stop = agg_for_monitor.config().force_completion_on_stop;
926 let consumer_handle = tokio::spawn(async move {
927 let _ = consumer_handle.await;
928 if !pipeline_cancel_for_monitor.is_cancelled() {
929 agg_for_monitor.force_complete_all();
930 if force_on_stop {
931 pipeline_cancel_for_monitor.cancel();
932 }
933 }
934 });
935 #[cfg(test)]
936 emit_start_route_event("consumer_spawned");
937
938 {
939 let managed = self
940 .routes
941 .get_mut(route_id)
942 .expect("invariant: route must exist"); managed.consumer_handle = Some(consumer_handle);
944 managed.pipeline_handle = Some(pipeline_handle);
945 managed.channel_sender = Some(tx_for_storage);
946 }
947
948 info!(route_id = %route_id, "Route started (aggregate with timeout)");
949 Ok(())
950 }
951
952 #[cfg(test)]
957 pub(crate) fn set_route_lifecycle_for_test(
958 &mut self,
959 route_id: &str,
960 lifecycle: Vec<Arc<dyn StepLifecycle>>,
961 ) -> Result<(), CamelError> {
962 use super::pipeline_runtime::PipelineAssembly;
963 use camel_api::SyncBoxProcessor;
964 use std::sync::Arc;
965
966 let managed = self
967 .routes
968 .get_mut(route_id)
969 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
970 let old_processor = managed.pipeline.load().processor.clone_inner();
971 managed.pipeline.store(Arc::new(PipelineAssembly::new(
972 SyncBoxProcessor::new(old_processor),
973 lifecycle,
974 )));
975 Ok(())
976 }
977}
978
979#[cfg(test)]
980impl crate::hot_reload::ports::ReloadIntrospectionPort for DefaultRouteController {
981 fn route_ids(&self) -> Vec<String> {
982 self.route_ids() }
984 fn route_from_uri(&self, route_id: &str) -> Option<String> {
985 self.route_from_uri(route_id) }
987 fn route_source_hash(&self, route_id: &str) -> Option<u64> {
988 self.route_source_hash(route_id) }
990}
991
992#[cfg(test)]
993#[path = "route_controller_tests.rs"]
994mod tests;