camel_core/lifecycle/adapters/
route_controller_trait.rs1use std::sync::Arc;
7use std::time::Duration;
8
9use tokio::sync::mpsc;
10use tokio_util::sync::CancellationToken;
11use tower::Service;
12use tracing::{error, info, warn};
13
14use camel_api::{CamelError, NoOpMetrics, StepLifecycle, StepShutdownReason};
15use camel_component_api::{ConcurrencyModel, ConsumerContext, consumer::ExchangeEnvelope};
16
17use crate::lifecycle::adapters::consumer_management;
18use crate::lifecycle::adapters::controller_component_context::ControllerComponentContext;
19use crate::lifecycle::adapters::route_compiler::CANCEL_TOKEN;
20use crate::lifecycle::adapters::route_controller::DefaultRouteController;
21#[cfg(test)]
22use crate::lifecycle::adapters::route_helpers::emit_start_route_event;
23use crate::lifecycle::adapters::route_helpers::{
24 DrainGuard, handle_is_running, inferred_lifecycle_label, ready_with_backoff,
25};
26use crate::lifecycle::adapters::route_registry::DEFAULT_SHUTDOWN_TIMEOUT;
27
28async fn rollback_started(route_id: &str, handles: &[Arc<dyn StepLifecycle>]) {
39 for handle in handles.iter().rev() {
40 if let Err(e) = handle.shutdown(StepShutdownReason::RouteStop).await {
41 warn!(
42 route_id = %route_id,
43 step = handle.name(),
44 error = %e,
45 "best-effort step shutdown during start rollback failed"
46 );
47 }
48 }
49}
50
51#[async_trait::async_trait]
52impl camel_api::RouteController for DefaultRouteController {
53 async fn start_route(&mut self, route_id: &str) -> Result<(), CamelError> {
54 {
56 let managed = self
57 .routes
58 .get_mut(route_id)
59 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
60
61 let consumer_running = handle_is_running(&managed.consumer_handle);
62 let pipeline_running = handle_is_running(&managed.pipeline_handle);
63 if consumer_running && pipeline_running {
64 return Ok(());
65 }
66 if !consumer_running && pipeline_running {
67 return Err(CamelError::RouteError(format!(
68 "Route '{}' is suspended; use resume_route() to resume, or stop_route() then start_route() for full restart",
69 route_id
70 )));
71 }
72 if consumer_running && !pipeline_running {
73 return Err(CamelError::RouteError(format!(
74 "Route '{}' has inconsistent execution state; stop_route() then retry start_route()",
75 route_id
76 )));
77 }
78 }
79
80 info!(route_id = %route_id, "Starting route");
81
82 let (from_uri, pipeline, concurrency) = {
84 let managed = self
85 .routes
86 .get(route_id)
87 .expect("invariant: route must exist after prior existence check"); (
89 managed.from_uri.clone(),
90 Arc::clone(&managed.pipeline),
91 managed.concurrency.clone(),
92 )
93 };
94
95 let lifecycle_handles: Vec<Arc<dyn StepLifecycle>> = pipeline.load().lifecycle.clone();
101 for (idx, handle) in lifecycle_handles.iter().enumerate() {
102 if let Err(start_err) = handle.start().await {
103 warn!(
104 route_id = %route_id,
105 step = handle.name(),
106 "step start failed; rolling back already-started steps"
107 );
108 rollback_started(route_id, &lifecycle_handles[0..idx]).await;
110 return Err(start_err);
111 }
112 }
113
114 let crash_notifier = self.crash_notifier.clone();
116 let runtime_for_consumer = self.runtime.clone();
117
118 let consumer_component_ctx = Arc::new(ControllerComponentContext::new(
119 Arc::clone(&self.registry),
120 Arc::clone(&self.languages),
121 self.tracer_metrics
122 .clone()
123 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
124 Arc::clone(&self.platform_service),
125 self.health_registry(),
126 Some(route_id.to_string()),
127 ));
128 let consumer_rt: Arc<dyn camel_component_api::RuntimeObservability> =
129 Arc::clone(&consumer_component_ctx) as Arc<_>;
130 let (mut consumer, consumer_concurrency) = match consumer_management::create_route_consumer(
131 consumer_rt,
132 &self.registry,
133 &from_uri,
134 consumer_component_ctx.as_ref(),
135 ) {
136 Ok(v) => v,
137 Err(e) => {
141 rollback_started(route_id, &lifecycle_handles).await;
142 return Err(e);
143 }
144 };
145
146 let effective_concurrency = concurrency.unwrap_or(consumer_concurrency);
148
149 let managed = self
151 .routes
152 .get_mut(route_id)
153 .expect("invariant: route must exist after prior existence check"); if let (Some(sp_config), Some(authenticator)) = (
157 managed.compiled.security_policy.as_ref(),
158 managed.compiled.security_authenticator.as_ref(),
159 ) {
160 use camel_component_api::SecurityContext;
161 let sec_ctx =
162 SecurityContext::from_arc(Arc::clone(&sp_config.policy), Arc::clone(authenticator));
163 consumer.set_security_context(sec_ctx);
164 }
165
166 let (tx, mut rx) = mpsc::channel::<ExchangeEnvelope>(256);
168 let consumer_cancel = managed.consumer_cancel_token.child_token();
170 let pipeline_cancel = managed.pipeline_cancel_token.child_token();
171 let drain_in_flight = Arc::clone(&managed.drain_in_flight);
172 let tx_for_storage = tx.clone();
174 let consumer_ctx = ConsumerContext::new(tx, consumer_cancel.clone(), route_id.to_string());
175
176 let split_clone = managed.aggregate_split.clone();
178 if let Some(split) = split_clone {
179 let result = self
180 .start_aggregate_route(
181 route_id,
182 split,
183 consumer,
184 consumer_ctx,
185 rx,
186 crash_notifier,
187 runtime_for_consumer,
188 tx_for_storage,
189 pipeline_cancel,
190 drain_in_flight,
191 )
192 .await;
193 if result.is_err() {
196 if let Some(managed) = self.routes.get_mut(route_id) {
199 managed.consumer_cancel_token.cancel();
200 }
201 rollback_started(route_id, &lifecycle_handles).await;
202 }
203 return result;
204 }
205 let pipeline_cancel_for_cleanup = pipeline_cancel.clone();
211
212 let pipeline_handle = match effective_concurrency {
214 ConcurrencyModel::Sequential => {
215 tokio::spawn(async move {
216 loop {
217 let envelope = tokio::select! {
219 envelope = rx.recv() => match envelope {
220 Some(e) => e,
221 None => return, },
223 _ = pipeline_cancel.cancelled() => {
224 return;
226 }
227 };
228 let ExchangeEnvelope { exchange, reply_tx } = envelope;
229
230 let mut pipeline = pipeline.load().processor.clone_inner();
232
233 if let Err(e) = ready_with_backoff(&mut pipeline, &pipeline_cancel).await {
234 if let Some(tx) = reply_tx {
235 let _ = tx.send(Err(e));
236 }
237 return;
238 }
239
240 let cancel = pipeline_cancel.clone();
246 let _drain_guard = DrainGuard::new(Arc::clone(&drain_in_flight));
247 let result = CANCEL_TOKEN
248 .scope(cancel, async move { pipeline.call(exchange).await })
249 .await;
250 if let Some(tx) = reply_tx {
251 let _ = tx.send(result);
252 } else if let Err(ref e) = result {
253 error!("Pipeline error: {e}");
255 }
256 }
257 })
258 }
259 ConcurrencyModel::Concurrent { max } => {
260 let sem = max.map(|n| Arc::new(tokio::sync::Semaphore::new(n)));
261 tokio::spawn(async move {
262 loop {
263 let permit = match &sem {
266 Some(s) => {
267 let acquired = tokio::select! {
268 p = Arc::clone(s).acquire_owned() => p.expect("semaphore closed"), _ = pipeline_cancel.cancelled() => return,
270 };
271 Some(acquired)
272 }
273 None => None,
274 };
275
276 let envelope = tokio::select! {
277 envelope = rx.recv() => match envelope {
278 Some(e) => e,
279 None => return,
280 },
281 _ = pipeline_cancel.cancelled() => return,
282 };
283 let ExchangeEnvelope { exchange, reply_tx } = envelope;
284 let pipe_ref = Arc::clone(&pipeline);
285 let cancel = pipeline_cancel.clone();
286 let drain_clone = Arc::clone(&drain_in_flight);
287 tokio::spawn(async move {
288 let _permit = permit;
290 let _drain_guard = DrainGuard::new(drain_clone);
291
292 let mut pipe = pipe_ref.load().processor.clone_inner();
294
295 if let Err(e) = ready_with_backoff(&mut pipe, &cancel).await {
297 if let Some(tx) = reply_tx {
298 let _ = tx.send(Err(e));
299 }
300 return;
301 }
302
303 let result = CANCEL_TOKEN
306 .scope(cancel, async move { pipe.call(exchange).await })
307 .await;
308 if let Some(tx) = reply_tx {
309 let _ = tx.send(result);
310 } else if let Err(ref e) = result {
311 error!("Pipeline error: {e}");
313 }
314 });
315 }
316 })
317 }
318 };
319 #[cfg(test)]
320 emit_start_route_event("pipeline_spawned");
321
322 let (consumer_handle, startup_rx) = consumer_management::spawn_consumer_task(
325 route_id.to_string(),
326 consumer,
327 consumer_ctx,
328 crash_notifier,
329 runtime_for_consumer,
330 false,
331 );
332 #[cfg(test)]
333 emit_start_route_event("consumer_spawned");
334
335 match consumer_management::await_consumer_startup(startup_rx, "startup").await {
340 Ok(()) => {}
341 Err(e) => {
342 consumer_handle.abort();
349 pipeline_cancel_for_cleanup.cancel();
350 consumer_cancel.cancel();
353 rollback_started(route_id, &lifecycle_handles).await;
354 return Err(e);
355 }
356 }
357
358 let managed = self
360 .routes
361 .get_mut(route_id)
362 .expect("invariant: route must exist after prior existence check"); managed.consumer_handle = Some(consumer_handle);
364 managed.pipeline_handle = Some(pipeline_handle);
365 managed.channel_sender = Some(tx_for_storage);
366
367 info!(route_id = %route_id, "Route started");
368 self.health_registry().mark_route_started(route_id);
369 Ok(())
370 }
371
372 async fn stop_route(&mut self, route_id: &str) -> Result<(), CamelError> {
373 self.stop_route_internal(route_id).await?;
374 self.health_registry().mark_route_stopped(route_id);
375 Ok(())
376 }
377
378 async fn restart_route(&mut self, route_id: &str) -> Result<(), CamelError> {
379 self.stop_route(route_id).await?;
380 tokio::time::sleep(Duration::from_millis(100)).await;
381 self.start_route(route_id).await
382 }
383
384 async fn suspend_route(&mut self, route_id: &str) -> Result<(), CamelError> {
385 let managed = self
387 .routes
388 .get_mut(route_id)
389 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
390
391 let consumer_running = handle_is_running(&managed.consumer_handle);
392 let pipeline_running = handle_is_running(&managed.pipeline_handle);
393
394 if !consumer_running || !pipeline_running {
396 return Err(CamelError::RouteError(format!(
397 "Cannot suspend route '{}' with execution lifecycle {}",
398 route_id,
399 inferred_lifecycle_label(managed)
400 )));
401 }
402
403 info!(route_id = %route_id, "Suspending route (consumer only, keeping pipeline)");
404
405 let managed = self
407 .routes
408 .get_mut(route_id)
409 .expect("invariant: route must exist after prior existence check"); managed.consumer_cancel_token.cancel();
411
412 let managed = self
414 .routes
415 .get_mut(route_id)
416 .expect("invariant: route must exist after prior existence check"); let consumer_handle = managed.consumer_handle.take();
418
419 let timeout_result = tokio::time::timeout(DEFAULT_SHUTDOWN_TIMEOUT, async {
421 if let Some(handle) = consumer_handle {
422 let _ = handle.await;
423 }
424 })
425 .await;
426
427 if timeout_result.is_err() {
428 warn!(route_id = %route_id, "Consumer shutdown timed out during suspend");
429 }
430
431 let managed = self
433 .routes
434 .get_mut(route_id)
435 .expect("invariant: route must exist after prior existence check"); managed.consumer_cancel_token = CancellationToken::new();
439
440 info!(route_id = %route_id, "Route suspended (pipeline still running)");
441 self.health_registry().mark_route_stopped(route_id);
442 Ok(())
443 }
444
445 async fn resume_route(&mut self, route_id: &str) -> Result<(), CamelError> {
446 let managed = self
448 .routes
449 .get(route_id)
450 .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
451
452 let consumer_running = handle_is_running(&managed.consumer_handle);
453 let pipeline_running = handle_is_running(&managed.pipeline_handle);
454 if consumer_running || !pipeline_running {
455 return Err(CamelError::RouteError(format!(
456 "Cannot resume route '{}' with execution lifecycle {} (expected Suspended)",
457 route_id,
458 inferred_lifecycle_label(managed)
459 )));
460 }
461
462 let sender = managed.channel_sender.clone().ok_or_else(|| {
464 CamelError::RouteError("Suspended route has no channel sender".into())
465 })?;
466
467 let from_uri = managed.from_uri.clone();
469
470 info!(route_id = %route_id, "Resuming route (spawning consumer only)");
471
472 let consumer_component_ctx = Arc::new(ControllerComponentContext::new(
473 Arc::clone(&self.registry),
474 Arc::clone(&self.languages),
475 self.tracer_metrics
476 .clone()
477 .unwrap_or_else(|| Arc::new(NoOpMetrics)),
478 Arc::clone(&self.platform_service),
479 self.health_registry(),
480 Some(route_id.to_string()),
481 ));
482 let consumer_rt: Arc<dyn camel_component_api::RuntimeObservability> =
483 Arc::clone(&consumer_component_ctx) as Arc<_>;
484 let (mut consumer, _) = consumer_management::create_route_consumer(
485 consumer_rt,
486 &self.registry,
487 &from_uri,
488 consumer_component_ctx.as_ref(),
489 )?;
490
491 let managed = self
493 .routes
494 .get(route_id)
495 .expect("invariant: route must exist after prior existence check"); if let (Some(sp_config), Some(authenticator)) = (
497 managed.compiled.security_policy.as_ref(),
498 managed.compiled.security_authenticator.as_ref(),
499 ) {
500 use camel_component_api::SecurityContext;
501 let sec_ctx =
502 SecurityContext::from_arc(Arc::clone(&sp_config.policy), Arc::clone(authenticator));
503 consumer.set_security_context(sec_ctx);
504 }
505
506 let managed = self
508 .routes
509 .get_mut(route_id)
510 .expect("invariant: route must exist after prior existence check"); let consumer_cancel = managed.consumer_cancel_token.child_token();
514
515 let crash_notifier = self.crash_notifier.clone();
516 let runtime_for_consumer = self.runtime.clone();
517
518 let consumer_ctx =
520 ConsumerContext::new(sender, consumer_cancel.clone(), route_id.to_string());
521
522 let (consumer_handle, startup_rx) = consumer_management::spawn_consumer_task(
524 route_id.to_string(),
525 consumer,
526 consumer_ctx,
527 crash_notifier,
528 runtime_for_consumer,
529 true,
530 );
531
532 consumer_management::await_consumer_startup(startup_rx, "resume").await?;
535
536 let managed = self
538 .routes
539 .get_mut(route_id)
540 .expect("invariant: route must exist after prior existence check"); managed.consumer_handle = Some(consumer_handle);
542
543 info!(route_id = %route_id, "Route resumed");
544 self.health_registry().mark_route_started(route_id);
545 Ok(())
546 }
547
548 async fn start_all_routes(&mut self) -> Result<(), CamelError> {
549 let route_ids: Vec<String> = {
552 let pairs = self.routes.auto_startup_sorted();
553 pairs.into_iter().map(|(id, _)| id).collect()
554 };
555
556 info!("Starting {} auto-startup routes", route_ids.len());
557
558 let mut errors: Vec<String> = Vec::new();
560 for route_id in route_ids {
561 if let Err(e) = self.start_route(&route_id).await {
562 errors.push(format!("Route '{}': {}", route_id, e));
563 }
564 }
565
566 if !errors.is_empty() {
567 return Err(CamelError::RouteError(format!(
568 "Failed to start routes: {}",
569 errors.join(", ")
570 )));
571 }
572
573 info!("All auto-startup routes started");
574 Ok(())
575 }
576
577 async fn stop_all_routes(&mut self) -> Result<(), CamelError> {
578 let route_ids: Vec<String> = {
580 let pairs = self.routes.shutdown_sorted();
581 pairs.into_iter().map(|(id, _)| id).collect()
582 };
583
584 info!("Stopping {} routes", route_ids.len());
585
586 for route_id in route_ids {
587 let _ = self.stop_route(&route_id).await;
588 }
589
590 info!("All routes stopped");
591 Ok(())
592 }
593}