Skip to main content

camel_core/lifecycle/adapters/
route_controller_trait.rs

1//! `RouteController` trait implementation for `DefaultRouteController`.
2//!
3//! Extracted from `route_controller.rs` to reduce file size. All lifecycle methods
4//! (start, stop, suspend, resume, etc.) live here.
5
6use 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};
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
28#[async_trait::async_trait]
29impl camel_api::RouteController for DefaultRouteController {
30    async fn start_route(&mut self, route_id: &str) -> Result<(), CamelError> {
31        // Check if route exists and can be started.
32        {
33            let managed = self
34                .routes
35                .get_mut(route_id)
36                .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
37
38            let consumer_running = handle_is_running(&managed.consumer_handle);
39            let pipeline_running = handle_is_running(&managed.pipeline_handle);
40            if consumer_running && pipeline_running {
41                return Ok(());
42            }
43            if !consumer_running && pipeline_running {
44                return Err(CamelError::RouteError(format!(
45                    "Route '{}' is suspended; use resume_route() to resume, or stop_route() then start_route() for full restart",
46                    route_id
47                )));
48            }
49            if consumer_running && !pipeline_running {
50                return Err(CamelError::RouteError(format!(
51                    "Route '{}' has inconsistent execution state; stop_route() then retry start_route()",
52                    route_id
53                )));
54            }
55        }
56
57        info!(route_id = %route_id, "Starting route");
58
59        // Get the resolved route info
60        let (from_uri, pipeline, concurrency) = {
61            let managed = self
62                .routes
63                .get(route_id)
64                .expect("invariant: route must exist after prior existence check"); // allow-unwrap
65            (
66                managed.from_uri.clone(),
67                Arc::clone(&managed.pipeline),
68                managed.concurrency.clone(),
69            )
70        };
71
72        // Clone crash notifier for consumer task
73        let crash_notifier = self.crash_notifier.clone();
74        let runtime_for_consumer = self.runtime.clone();
75
76        let consumer_component_ctx = Arc::new(ControllerComponentContext::new(
77            Arc::clone(&self.registry),
78            Arc::clone(&self.languages),
79            self.tracer_metrics
80                .clone()
81                .unwrap_or_else(|| Arc::new(NoOpMetrics)),
82            Arc::clone(&self.platform_service),
83            self.health_registry(),
84            Some(route_id.to_string()),
85        ));
86        let consumer_rt: Arc<dyn camel_component_api::RuntimeObservability> =
87            Arc::clone(&consumer_component_ctx) as Arc<_>;
88        let (mut consumer, consumer_concurrency) = consumer_management::create_route_consumer(
89            consumer_rt,
90            &self.registry,
91            &from_uri,
92            consumer_component_ctx.as_ref(),
93        )?;
94
95        // Resolve effective concurrency: route override > consumer default
96        let effective_concurrency = concurrency.unwrap_or(consumer_concurrency);
97
98        // Get the managed route for mutation
99        let managed = self
100            .routes
101            .get_mut(route_id)
102            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
103
104        // Wire security context before spawning consumer
105        if let (Some(sp_config), Some(authenticator)) = (
106            managed.compiled.security_policy.as_ref(),
107            managed.compiled.security_authenticator.as_ref(),
108        ) {
109            use camel_component_api::SecurityContext;
110            let sec_ctx =
111                SecurityContext::from_arc(Arc::clone(&sp_config.policy), Arc::clone(authenticator));
112            consumer.set_security_context(sec_ctx);
113        }
114
115        // Create channel for consumer to send exchanges
116        let (tx, mut rx) = mpsc::channel::<ExchangeEnvelope>(256);
117        // Create child tokens for independent lifecycle control
118        let consumer_cancel = managed.consumer_cancel_token.child_token();
119        let pipeline_cancel = managed.pipeline_cancel_token.child_token();
120        let drain_in_flight = Arc::clone(&managed.drain_in_flight);
121        // Clone sender for storage (to reuse on resume)
122        let tx_for_storage = tx.clone();
123        let consumer_ctx = ConsumerContext::new(tx, consumer_cancel.clone(), route_id.to_string());
124
125        // --- Aggregator v2: check for aggregate route with timeout ---
126        let split_clone = managed.aggregate_split.clone();
127        if let Some(split) = split_clone {
128            return self
129                .start_aggregate_route(
130                    route_id,
131                    split,
132                    consumer,
133                    consumer_ctx,
134                    rx,
135                    crash_notifier,
136                    runtime_for_consumer,
137                    tx_for_storage,
138                    pipeline_cancel,
139                    drain_in_flight,
140                )
141                .await;
142        }
143        // --- End aggregator v2 branch ---
144
145        // Spawn pipeline task with its own cancellation token
146        let pipeline_handle = match effective_concurrency {
147            ConcurrencyModel::Sequential => {
148                tokio::spawn(async move {
149                    loop {
150                        // Use select! to exit promptly on cancellation even when idle
151                        let envelope = tokio::select! {
152                            envelope = rx.recv() => match envelope {
153                                Some(e) => e,
154                                None => return, // Channel closed
155                            },
156                            _ = pipeline_cancel.cancelled() => {
157                                // Cancellation requested - exit gracefully
158                                return;
159                            }
160                        };
161                        let ExchangeEnvelope { exchange, reply_tx } = envelope;
162
163                        // Load current pipeline from ArcSwap (picks up hot-reloaded pipelines)
164                        let mut pipeline = pipeline.load().processor.clone_inner();
165
166                        if let Err(e) = ready_with_backoff(&mut pipeline, &pipeline_cancel).await {
167                            if let Some(tx) = reply_tx {
168                                let _ = tx.send(Err(e));
169                            }
170                            return;
171                        }
172
173                        // B1: scope CANCEL_TOKEN so run_steps can check cancellation
174                        // between steps. Per-start task-local — child token expires
175                        // when this pipeline task exits; the next start re-scopes a
176                        // fresh one (avoids the lifecycle bug where a compiled-in
177                        // child token stays cancelled after stop→restart).
178                        let cancel = pipeline_cancel.clone();
179                        let _drain_guard = DrainGuard::new(Arc::clone(&drain_in_flight));
180                        let result = CANCEL_TOKEN
181                            .scope(cancel, async move { pipeline.call(exchange).await })
182                            .await;
183                        if let Some(tx) = reply_tx {
184                            let _ = tx.send(result);
185                        } else if let Err(ref e) = result {
186                            // log-policy: system-broken
187                            error!("Pipeline error: {e}");
188                        }
189                    }
190                })
191            }
192            ConcurrencyModel::Concurrent { max } => {
193                let sem = max.map(|n| Arc::new(tokio::sync::Semaphore::new(n)));
194                tokio::spawn(async move {
195                    loop {
196                        // B2 (ADR-0044): acquire permit BEFORE dequeue.
197                        // Cancel-aware: route stop is not blocked waiting for a permit.
198                        let permit = match &sem {
199                            Some(s) => {
200                                let acquired = tokio::select! {
201                                    p = Arc::clone(s).acquire_owned() => p.expect("semaphore closed"), // allow-unwrap
202                                    _ = pipeline_cancel.cancelled() => return,
203                                };
204                                Some(acquired)
205                            }
206                            None => None,
207                        };
208
209                        let envelope = tokio::select! {
210                            envelope = rx.recv() => match envelope {
211                                Some(e) => e,
212                                None => return,
213                            },
214                            _ = pipeline_cancel.cancelled() => return,
215                        };
216                        let ExchangeEnvelope { exchange, reply_tx } = envelope;
217                        let pipe_ref = Arc::clone(&pipeline);
218                        let cancel = pipeline_cancel.clone();
219                        let drain_clone = Arc::clone(&drain_in_flight);
220                        tokio::spawn(async move {
221                            // Permit owned by this task — released on completion (RAII).
222                            let _permit = permit;
223                            let _drain_guard = DrainGuard::new(drain_clone);
224
225                            // Load current pipeline from ArcSwap
226                            let mut pipe = pipe_ref.load().processor.clone_inner();
227
228                            // Wait for service ready with circuit breaker backoff
229                            if let Err(e) = ready_with_backoff(&mut pipe, &cancel).await {
230                                if let Some(tx) = reply_tx {
231                                    let _ = tx.send(Err(e));
232                                }
233                                return;
234                            }
235
236                            // B1: scope CANCEL_TOKEN so run_steps can check
237                            // cancellation between steps.
238                            let result = CANCEL_TOKEN
239                                .scope(cancel, async move { pipe.call(exchange).await })
240                                .await;
241                            if let Some(tx) = reply_tx {
242                                let _ = tx.send(result);
243                            } else if let Err(ref e) = result {
244                                // log-policy: system-broken
245                                error!("Pipeline error: {e}");
246                            }
247                        });
248                    }
249                })
250            }
251        };
252        #[cfg(test)]
253        emit_start_route_event("pipeline_spawned");
254
255        // Start consumer after pipeline task is spawned to minimize the chance of
256        // fire-and-forget events being produced before the pipeline loop is active.
257        let consumer_handle = consumer_management::spawn_consumer_task(
258            route_id.to_string(),
259            consumer,
260            consumer_ctx,
261            crash_notifier,
262            runtime_for_consumer,
263            false,
264        );
265        #[cfg(test)]
266        emit_start_route_event("consumer_spawned");
267
268        // Store handles and update status
269        let managed = self
270            .routes
271            .get_mut(route_id)
272            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
273        managed.consumer_handle = Some(consumer_handle);
274        managed.pipeline_handle = Some(pipeline_handle);
275        managed.channel_sender = Some(tx_for_storage);
276
277        info!(route_id = %route_id, "Route started");
278        self.health_registry().mark_route_started(route_id);
279        Ok(())
280    }
281
282    async fn stop_route(&mut self, route_id: &str) -> Result<(), CamelError> {
283        self.stop_route_internal(route_id).await?;
284        self.health_registry().mark_route_stopped(route_id);
285        Ok(())
286    }
287
288    async fn restart_route(&mut self, route_id: &str) -> Result<(), CamelError> {
289        self.stop_route(route_id).await?;
290        tokio::time::sleep(Duration::from_millis(100)).await;
291        self.start_route(route_id).await
292    }
293
294    async fn suspend_route(&mut self, route_id: &str) -> Result<(), CamelError> {
295        // Check route exists and state.
296        let managed = self
297            .routes
298            .get_mut(route_id)
299            .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
300
301        let consumer_running = handle_is_running(&managed.consumer_handle);
302        let pipeline_running = handle_is_running(&managed.pipeline_handle);
303
304        // Can only suspend from active started state.
305        if !consumer_running || !pipeline_running {
306            return Err(CamelError::RouteError(format!(
307                "Cannot suspend route '{}' with execution lifecycle {}",
308                route_id,
309                inferred_lifecycle_label(managed)
310            )));
311        }
312
313        info!(route_id = %route_id, "Suspending route (consumer only, keeping pipeline)");
314
315        // Cancel consumer token only (keep pipeline running)
316        let managed = self
317            .routes
318            .get_mut(route_id)
319            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
320        managed.consumer_cancel_token.cancel();
321
322        // Take and join consumer handle
323        let managed = self
324            .routes
325            .get_mut(route_id)
326            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
327        let consumer_handle = managed.consumer_handle.take();
328
329        // Wait for consumer task to complete with timeout
330        let timeout_result = tokio::time::timeout(DEFAULT_SHUTDOWN_TIMEOUT, async {
331            if let Some(handle) = consumer_handle {
332                let _ = handle.await;
333            }
334        })
335        .await;
336
337        if timeout_result.is_err() {
338            warn!(route_id = %route_id, "Consumer shutdown timed out during suspend");
339        }
340
341        // Get the managed route again (can't hold across await)
342        let managed = self
343            .routes
344            .get_mut(route_id)
345            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
346
347        // Create fresh cancellation token for consumer (for resume)
348        managed.consumer_cancel_token = CancellationToken::new();
349
350        info!(route_id = %route_id, "Route suspended (pipeline still running)");
351        self.health_registry().mark_route_stopped(route_id);
352        Ok(())
353    }
354
355    async fn resume_route(&mut self, route_id: &str) -> Result<(), CamelError> {
356        // Check route exists and is Suspended-equivalent execution state.
357        let managed = self
358            .routes
359            .get(route_id)
360            .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
361
362        let consumer_running = handle_is_running(&managed.consumer_handle);
363        let pipeline_running = handle_is_running(&managed.pipeline_handle);
364        if consumer_running || !pipeline_running {
365            return Err(CamelError::RouteError(format!(
366                "Cannot resume route '{}' with execution lifecycle {} (expected Suspended)",
367                route_id,
368                inferred_lifecycle_label(managed)
369            )));
370        }
371
372        // Get the stored channel sender (must exist for a suspended route)
373        let sender = managed.channel_sender.clone().ok_or_else(|| {
374            CamelError::RouteError("Suspended route has no channel sender".into())
375        })?;
376
377        // Get from_uri and concurrency for creating new consumer
378        let from_uri = managed.from_uri.clone();
379
380        info!(route_id = %route_id, "Resuming route (spawning consumer only)");
381
382        let consumer_component_ctx = Arc::new(ControllerComponentContext::new(
383            Arc::clone(&self.registry),
384            Arc::clone(&self.languages),
385            self.tracer_metrics
386                .clone()
387                .unwrap_or_else(|| Arc::new(NoOpMetrics)),
388            Arc::clone(&self.platform_service),
389            self.health_registry(),
390            Some(route_id.to_string()),
391        ));
392        let consumer_rt: Arc<dyn camel_component_api::RuntimeObservability> =
393            Arc::clone(&consumer_component_ctx) as Arc<_>;
394        let (mut consumer, _) = consumer_management::create_route_consumer(
395            consumer_rt,
396            &self.registry,
397            &from_uri,
398            consumer_component_ctx.as_ref(),
399        )?;
400
401        // Wire security context before spawning consumer
402        let managed = self
403            .routes
404            .get(route_id)
405            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
406        if let (Some(sp_config), Some(authenticator)) = (
407            managed.compiled.security_policy.as_ref(),
408            managed.compiled.security_authenticator.as_ref(),
409        ) {
410            use camel_component_api::SecurityContext;
411            let sec_ctx =
412                SecurityContext::from_arc(Arc::clone(&sp_config.policy), Arc::clone(authenticator));
413            consumer.set_security_context(sec_ctx);
414        }
415
416        // Get the managed route for mutation
417        let managed = self
418            .routes
419            .get_mut(route_id)
420            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
421
422        // Create child token for consumer lifecycle
423        let consumer_cancel = managed.consumer_cancel_token.child_token();
424
425        let crash_notifier = self.crash_notifier.clone();
426        let runtime_for_consumer = self.runtime.clone();
427
428        // Create ConsumerContext with the stored sender
429        let consumer_ctx =
430            ConsumerContext::new(sender, consumer_cancel.clone(), route_id.to_string());
431
432        // Spawn consumer task
433        let consumer_handle = consumer_management::spawn_consumer_task(
434            route_id.to_string(),
435            consumer,
436            consumer_ctx,
437            crash_notifier,
438            runtime_for_consumer,
439            true,
440        );
441
442        // Store consumer handle and update status
443        let managed = self
444            .routes
445            .get_mut(route_id)
446            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
447        managed.consumer_handle = Some(consumer_handle);
448
449        info!(route_id = %route_id, "Route resumed");
450        self.health_registry().mark_route_started(route_id);
451        Ok(())
452    }
453
454    async fn start_all_routes(&mut self) -> Result<(), CamelError> {
455        // Only start routes where auto_startup() == true
456        // Sort by startup_order() ascending before starting
457        let route_ids: Vec<String> = {
458            let pairs = self.routes.auto_startup_sorted();
459            pairs.into_iter().map(|(id, _)| id).collect()
460        };
461
462        info!("Starting {} auto-startup routes", route_ids.len());
463
464        // Collect errors but continue starting remaining routes
465        let mut errors: Vec<String> = Vec::new();
466        for route_id in route_ids {
467            if let Err(e) = self.start_route(&route_id).await {
468                errors.push(format!("Route '{}': {}", route_id, e));
469            }
470        }
471
472        if !errors.is_empty() {
473            return Err(CamelError::RouteError(format!(
474                "Failed to start routes: {}",
475                errors.join(", ")
476            )));
477        }
478
479        info!("All auto-startup routes started");
480        Ok(())
481    }
482
483    async fn stop_all_routes(&mut self) -> Result<(), CamelError> {
484        // Sort by startup_order descending (reverse order)
485        let route_ids: Vec<String> = {
486            let pairs = self.routes.shutdown_sorted();
487            pairs.into_iter().map(|(id, _)| id).collect()
488        };
489
490        info!("Stopping {} routes", route_ids.len());
491
492        for route_id in route_ids {
493            let _ = self.stop_route(&route_id).await;
494        }
495
496        info!("All routes stopped");
497        Ok(())
498    }
499}