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