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