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, 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
28/// Best-effort, reverse-order shutdown of already-started `StepLifecycle`
29/// handles when `start_route` must abort. Used both mid-start-loop (the
30/// `[0..idx)` already-started prefix) and for any post-start failure path
31/// (e.g. `create_route_consumer`, the aggregate spawn branch, the consumer
32/// startup handshake) so the ADR-0022 SPI holds: if `start_route` returns
33/// `Err`, no started handle is left running.
34///
35/// Mirrors `StepLifecycle::shutdown`'s best-effort contract — each error is
36/// logged and swallowed so one failing shutdown cannot block rollback of the
37/// remaining handles.
38async 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        // Check if route exists and can be started.
55        {
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        // Get the resolved route info
83        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"); // allow-unwrap
88            (
89                managed.from_uri.clone(),
90                Arc::clone(&managed.pipeline),
91                managed.concurrency.clone(),
92            )
93        };
94
95        // ADR-0022: await each stateful step's `start()` before spawning the
96        // pipeline or consumer. On the Nth failure, roll back the already-
97        // started steps in reverse order (best-effort) and return the original
98        // start error WITHOUT spawning anything. Handles come from the compiled
99        // pipeline assembly, already collected in route order at compile time.
100        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                // Only [0..idx) have started; the Nth handle itself never did.
109                rollback_started(route_id, &lifecycle_handles[0..idx]).await;
110                return Err(start_err);
111            }
112        }
113
114        // Clone crash notifier for consumer task
115        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            // ADR-0022 SPI: every started handle must be rolled back
138            // before start_route returns Err, so no stateful step is left
139            // running. This is the first post-start fallible step.
140            Err(e) => {
141                rollback_started(route_id, &lifecycle_handles).await;
142                return Err(e);
143            }
144        };
145
146        // Resolve effective concurrency: route override > consumer default
147        let effective_concurrency = concurrency.unwrap_or(consumer_concurrency);
148
149        // Get the managed route for mutation
150        let managed = self
151            .routes
152            .get_mut(route_id)
153            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
154
155        // Wire security context before spawning consumer
156        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        // Create channel for consumer to send exchanges
167        let (tx, mut rx) = mpsc::channel::<ExchangeEnvelope>(256);
168        // Create child tokens for independent lifecycle control
169        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        // Clone sender for storage (to reuse on resume)
173        let tx_for_storage = tx.clone();
174        let consumer_ctx = ConsumerContext::new(tx, consumer_cancel.clone(), route_id.to_string());
175
176        // --- Aggregator v2: check for aggregate route with timeout ---
177        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            // ADR-0022 SPI: roll back already-started handles if the aggregate
194            // spawn/startup path returns Err.
195            if result.is_err() {
196                // rc-kh7c: cancel consumer's cancel token to stop child tasks
197                // spawned by consumer.start() that observe ctx.cancelled().
198                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        // --- End aggregator v2 branch ---
206
207        // Clone for the startup-failure cleanup path (rc-kh7c): pipeline_cancel
208        // is moved into the spawn closure below; this clone stays in scope so
209        // the error handler can cancel it to force immediate pipeline exit.
210        let pipeline_cancel_for_cleanup = pipeline_cancel.clone();
211
212        // Spawn pipeline task with its own cancellation token
213        let pipeline_handle = match effective_concurrency {
214            ConcurrencyModel::Sequential => {
215                tokio::spawn(async move {
216                    loop {
217                        // Use select! to exit promptly on cancellation even when idle
218                        let envelope = tokio::select! {
219                            envelope = rx.recv() => match envelope {
220                                Some(e) => e,
221                                None => return, // Channel closed
222                            },
223                            _ = pipeline_cancel.cancelled() => {
224                                // Cancellation requested - exit gracefully
225                                return;
226                            }
227                        };
228                        let ExchangeEnvelope { exchange, reply_tx } = envelope;
229
230                        // Load current pipeline from ArcSwap (picks up hot-reloaded pipelines)
231                        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                        // B1: scope CANCEL_TOKEN so run_steps can check cancellation
241                        // between steps. Per-start task-local — child token expires
242                        // when this pipeline task exits; the next start re-scopes a
243                        // fresh one (avoids the lifecycle bug where a compiled-in
244                        // child token stays cancelled after stop→restart).
245                        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                            // log-policy: system-broken
254                            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                        // B2 (ADR-0044): acquire permit BEFORE dequeue.
264                        // Cancel-aware: route stop is not blocked waiting for a permit.
265                        let permit = match &sem {
266                            Some(s) => {
267                                let acquired = tokio::select! {
268                                    p = Arc::clone(s).acquire_owned() => p.expect("semaphore closed"), // allow-unwrap
269                                    _ = 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                            // Permit owned by this task — released on completion (RAII).
289                            let _permit = permit;
290                            let _drain_guard = DrainGuard::new(drain_clone);
291
292                            // Load current pipeline from ArcSwap
293                            let mut pipe = pipe_ref.load().processor.clone_inner();
294
295                            // Wait for service ready with circuit breaker backoff
296                            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                            // B1: scope CANCEL_TOKEN so run_steps can check
304                            // cancellation between steps.
305                            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                                // log-policy: system-broken
312                                error!("Pipeline error: {e}");
313                            }
314                        });
315                    }
316                })
317            }
318        };
319        #[cfg(test)]
320        emit_start_route_event("pipeline_spawned");
321
322        // Start consumer after pipeline task is spawned to minimize the chance of
323        // fire-and-forget events being produced before the pipeline loop is active.
324        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        // rc-w1u9: await consumer startup handshake before returning. For
336        // Immediate consumers this is a no-op (pre-resolved receiver); for
337        // Explicit consumers (HTTP, WebSocket) it propagates bind failures as
338        // proper startup errors instead of silent background logs.
339        match consumer_management::await_consumer_startup(startup_rx, "startup").await {
340            Ok(()) => {}
341            Err(e) => {
342                // rc-kh7c: abort the orphaned consumer task and cancel the
343                // pipeline so neither runs detached after start_route returns
344                // Err. Dropping a JoinHandle detaches the task (Tokio
345                // contract); abort() forces termination. The pipeline task
346                // would eventually self-clean via rx-drop, but explicit
347                // cancellation makes it immediate.
348                consumer_handle.abort();
349                pipeline_cancel_for_cleanup.cancel();
350                // Cancel the consumer's cancel token so child tasks spawned
351                // by consumer.start() that observe ctx.cancelled() also stop.
352                consumer_cancel.cancel();
353                rollback_started(route_id, &lifecycle_handles).await;
354                return Err(e);
355            }
356        }
357
358        // Store handles and update status
359        let managed = self
360            .routes
361            .get_mut(route_id)
362            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
363        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        // Check route exists and state.
386        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        // Can only suspend from active started state.
395        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        // Cancel consumer token only (keep pipeline running)
406        let managed = self
407            .routes
408            .get_mut(route_id)
409            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
410        managed.consumer_cancel_token.cancel();
411
412        // Take and join consumer handle
413        let managed = self
414            .routes
415            .get_mut(route_id)
416            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
417        let consumer_handle = managed.consumer_handle.take();
418
419        // Wait for consumer task to complete with timeout
420        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        // Get the managed route again (can't hold across await)
432        let managed = self
433            .routes
434            .get_mut(route_id)
435            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
436
437        // Create fresh cancellation token for consumer (for resume)
438        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        // Check route exists and is Suspended-equivalent execution state.
447        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        // Get the stored channel sender (must exist for a suspended route)
463        let sender = managed.channel_sender.clone().ok_or_else(|| {
464            CamelError::RouteError("Suspended route has no channel sender".into())
465        })?;
466
467        // Get from_uri and concurrency for creating new consumer
468        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        // Wire security context before spawning consumer
492        let managed = self
493            .routes
494            .get(route_id)
495            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
496        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        // Get the managed route for mutation
507        let managed = self
508            .routes
509            .get_mut(route_id)
510            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
511
512        // Create child token for consumer lifecycle
513        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        // Create ConsumerContext with the stored sender
519        let consumer_ctx =
520            ConsumerContext::new(sender, consumer_cancel.clone(), route_id.to_string());
521
522        // Spawn consumer task
523        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        // rc-w1u9: await consumer startup handshake on resume too — bind
533        // failures during resume must surface as resume errors.
534        consumer_management::await_consumer_startup(startup_rx, "resume").await?;
535
536        // Store consumer handle and update status
537        let managed = self
538            .routes
539            .get_mut(route_id)
540            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
541        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        // Only start routes where auto_startup() == true
550        // Sort by startup_order() ascending before starting
551        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        // Collect errors but continue starting remaining routes
559        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        // Sort by startup_order descending (reverse order)
579        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}