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, startup_rx) = 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        // rc-w1u9: await consumer startup handshake before returning. For
269        // Immediate consumers this is a no-op (pre-resolved receiver); for
270        // Explicit consumers (HTTP, WebSocket) it propagates bind failures as
271        // proper startup errors instead of silent background logs.
272        consumer_management::await_consumer_startup(startup_rx, "startup").await?;
273
274        // Store handles and update status
275        let managed = self
276            .routes
277            .get_mut(route_id)
278            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
279        managed.consumer_handle = Some(consumer_handle);
280        managed.pipeline_handle = Some(pipeline_handle);
281        managed.channel_sender = Some(tx_for_storage);
282
283        info!(route_id = %route_id, "Route started");
284        self.health_registry().mark_route_started(route_id);
285        Ok(())
286    }
287
288    async fn stop_route(&mut self, route_id: &str) -> Result<(), CamelError> {
289        self.stop_route_internal(route_id).await?;
290        self.health_registry().mark_route_stopped(route_id);
291        Ok(())
292    }
293
294    async fn restart_route(&mut self, route_id: &str) -> Result<(), CamelError> {
295        self.stop_route(route_id).await?;
296        tokio::time::sleep(Duration::from_millis(100)).await;
297        self.start_route(route_id).await
298    }
299
300    async fn suspend_route(&mut self, route_id: &str) -> Result<(), CamelError> {
301        // Check route exists and state.
302        let managed = self
303            .routes
304            .get_mut(route_id)
305            .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
306
307        let consumer_running = handle_is_running(&managed.consumer_handle);
308        let pipeline_running = handle_is_running(&managed.pipeline_handle);
309
310        // Can only suspend from active started state.
311        if !consumer_running || !pipeline_running {
312            return Err(CamelError::RouteError(format!(
313                "Cannot suspend route '{}' with execution lifecycle {}",
314                route_id,
315                inferred_lifecycle_label(managed)
316            )));
317        }
318
319        info!(route_id = %route_id, "Suspending route (consumer only, keeping pipeline)");
320
321        // Cancel consumer token only (keep pipeline running)
322        let managed = self
323            .routes
324            .get_mut(route_id)
325            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
326        managed.consumer_cancel_token.cancel();
327
328        // Take and join consumer handle
329        let managed = self
330            .routes
331            .get_mut(route_id)
332            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
333        let consumer_handle = managed.consumer_handle.take();
334
335        // Wait for consumer task to complete with timeout
336        let timeout_result = tokio::time::timeout(DEFAULT_SHUTDOWN_TIMEOUT, async {
337            if let Some(handle) = consumer_handle {
338                let _ = handle.await;
339            }
340        })
341        .await;
342
343        if timeout_result.is_err() {
344            warn!(route_id = %route_id, "Consumer shutdown timed out during suspend");
345        }
346
347        // Get the managed route again (can't hold across await)
348        let managed = self
349            .routes
350            .get_mut(route_id)
351            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
352
353        // Create fresh cancellation token for consumer (for resume)
354        managed.consumer_cancel_token = CancellationToken::new();
355
356        info!(route_id = %route_id, "Route suspended (pipeline still running)");
357        self.health_registry().mark_route_stopped(route_id);
358        Ok(())
359    }
360
361    async fn resume_route(&mut self, route_id: &str) -> Result<(), CamelError> {
362        // Check route exists and is Suspended-equivalent execution state.
363        let managed = self
364            .routes
365            .get(route_id)
366            .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
367
368        let consumer_running = handle_is_running(&managed.consumer_handle);
369        let pipeline_running = handle_is_running(&managed.pipeline_handle);
370        if consumer_running || !pipeline_running {
371            return Err(CamelError::RouteError(format!(
372                "Cannot resume route '{}' with execution lifecycle {} (expected Suspended)",
373                route_id,
374                inferred_lifecycle_label(managed)
375            )));
376        }
377
378        // Get the stored channel sender (must exist for a suspended route)
379        let sender = managed.channel_sender.clone().ok_or_else(|| {
380            CamelError::RouteError("Suspended route has no channel sender".into())
381        })?;
382
383        // Get from_uri and concurrency for creating new consumer
384        let from_uri = managed.from_uri.clone();
385
386        info!(route_id = %route_id, "Resuming route (spawning consumer only)");
387
388        let consumer_component_ctx = Arc::new(ControllerComponentContext::new(
389            Arc::clone(&self.registry),
390            Arc::clone(&self.languages),
391            self.tracer_metrics
392                .clone()
393                .unwrap_or_else(|| Arc::new(NoOpMetrics)),
394            Arc::clone(&self.platform_service),
395            self.health_registry(),
396            Some(route_id.to_string()),
397        ));
398        let consumer_rt: Arc<dyn camel_component_api::RuntimeObservability> =
399            Arc::clone(&consumer_component_ctx) as Arc<_>;
400        let (mut consumer, _) = consumer_management::create_route_consumer(
401            consumer_rt,
402            &self.registry,
403            &from_uri,
404            consumer_component_ctx.as_ref(),
405        )?;
406
407        // Wire security context before spawning consumer
408        let managed = self
409            .routes
410            .get(route_id)
411            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
412        if let (Some(sp_config), Some(authenticator)) = (
413            managed.compiled.security_policy.as_ref(),
414            managed.compiled.security_authenticator.as_ref(),
415        ) {
416            use camel_component_api::SecurityContext;
417            let sec_ctx =
418                SecurityContext::from_arc(Arc::clone(&sp_config.policy), Arc::clone(authenticator));
419            consumer.set_security_context(sec_ctx);
420        }
421
422        // Get the managed route for mutation
423        let managed = self
424            .routes
425            .get_mut(route_id)
426            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
427
428        // Create child token for consumer lifecycle
429        let consumer_cancel = managed.consumer_cancel_token.child_token();
430
431        let crash_notifier = self.crash_notifier.clone();
432        let runtime_for_consumer = self.runtime.clone();
433
434        // Create ConsumerContext with the stored sender
435        let consumer_ctx =
436            ConsumerContext::new(sender, consumer_cancel.clone(), route_id.to_string());
437
438        // Spawn consumer task
439        let (consumer_handle, startup_rx) = consumer_management::spawn_consumer_task(
440            route_id.to_string(),
441            consumer,
442            consumer_ctx,
443            crash_notifier,
444            runtime_for_consumer,
445            true,
446        );
447
448        // rc-w1u9: await consumer startup handshake on resume too — bind
449        // failures during resume must surface as resume errors.
450        consumer_management::await_consumer_startup(startup_rx, "resume").await?;
451
452        // Store consumer handle and update status
453        let managed = self
454            .routes
455            .get_mut(route_id)
456            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
457        managed.consumer_handle = Some(consumer_handle);
458
459        info!(route_id = %route_id, "Route resumed");
460        self.health_registry().mark_route_started(route_id);
461        Ok(())
462    }
463
464    async fn start_all_routes(&mut self) -> Result<(), CamelError> {
465        // Only start routes where auto_startup() == true
466        // Sort by startup_order() ascending before starting
467        let route_ids: Vec<String> = {
468            let pairs = self.routes.auto_startup_sorted();
469            pairs.into_iter().map(|(id, _)| id).collect()
470        };
471
472        info!("Starting {} auto-startup routes", route_ids.len());
473
474        // Collect errors but continue starting remaining routes
475        let mut errors: Vec<String> = Vec::new();
476        for route_id in route_ids {
477            if let Err(e) = self.start_route(&route_id).await {
478                errors.push(format!("Route '{}': {}", route_id, e));
479            }
480        }
481
482        if !errors.is_empty() {
483            return Err(CamelError::RouteError(format!(
484                "Failed to start routes: {}",
485                errors.join(", ")
486            )));
487        }
488
489        info!("All auto-startup routes started");
490        Ok(())
491    }
492
493    async fn stop_all_routes(&mut self) -> Result<(), CamelError> {
494        // Sort by startup_order descending (reverse order)
495        let route_ids: Vec<String> = {
496            let pairs = self.routes.shutdown_sorted();
497            pairs.into_iter().map(|(id, _)| id).collect()
498        };
499
500        info!("Stopping {} routes", route_ids.len());
501
502        for route_id in route_ids {
503            let _ = self.stop_route(&route_id).await;
504        }
505
506        info!("All routes stopped");
507        Ok(())
508    }
509}