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                    .with_credential_sources(sp_config.credential_sources.clone());
164            consumer.set_security_context(sec_ctx);
165        }
166
167        // Create channel for consumer to send exchanges
168        let (tx, mut rx) = mpsc::channel::<ExchangeEnvelope>(256);
169        // Create child tokens for independent lifecycle control
170        let consumer_cancel = managed.consumer_cancel_token.child_token();
171        let pipeline_cancel = managed.pipeline_cancel_token.child_token();
172        let drain_in_flight = Arc::clone(&managed.drain_in_flight);
173        // Clone sender for storage (to reuse on resume)
174        let tx_for_storage = tx.clone();
175        let consumer_ctx = ConsumerContext::new(tx, consumer_cancel.clone(), route_id.to_string());
176
177        // --- Aggregator v2: check for aggregate route with timeout ---
178        let split_clone = managed.aggregate_split.clone();
179        if let Some(split) = split_clone {
180            let result = self
181                .start_aggregate_route(
182                    route_id,
183                    split,
184                    consumer,
185                    consumer_ctx,
186                    rx,
187                    crash_notifier,
188                    runtime_for_consumer,
189                    tx_for_storage,
190                    pipeline_cancel,
191                    drain_in_flight,
192                )
193                .await;
194            // ADR-0022 SPI: roll back already-started handles if the aggregate
195            // spawn/startup path returns Err.
196            if result.is_err() {
197                // rc-kh7c: cancel consumer's cancel token to stop child tasks
198                // spawned by consumer.start() that observe ctx.cancelled().
199                if let Some(managed) = self.routes.get_mut(route_id) {
200                    managed.consumer_cancel_token.cancel();
201                }
202                rollback_started(route_id, &lifecycle_handles).await;
203            }
204            return result;
205        }
206        // --- End aggregator v2 branch ---
207
208        // Clone for the startup-failure cleanup path (rc-kh7c): pipeline_cancel
209        // is moved into the spawn closure below; this clone stays in scope so
210        // the error handler can cancel it to force immediate pipeline exit.
211        let pipeline_cancel_for_cleanup = pipeline_cancel.clone();
212
213        // Spawn pipeline task with its own cancellation token
214        let pipeline_handle = match effective_concurrency {
215            ConcurrencyModel::Concurrent { max } => {
216                let sem = max.map(|n| Arc::new(tokio::sync::Semaphore::new(n)));
217                tokio::spawn(async move {
218                    loop {
219                        // B2 (ADR-0044): acquire permit BEFORE dequeue.
220                        // Cancel-aware: route stop is not blocked waiting for a permit.
221                        let permit = match &sem {
222                            Some(s) => {
223                                let acquired = tokio::select! {
224                                    p = Arc::clone(s).acquire_owned() => p.expect("semaphore closed"), // allow-unwrap
225                                    _ = pipeline_cancel.cancelled() => return,
226                                };
227                                Some(acquired)
228                            }
229                            None => None,
230                        };
231
232                        let envelope = tokio::select! {
233                            envelope = rx.recv() => match envelope {
234                                Some(e) => e,
235                                None => return,
236                            },
237                            _ = pipeline_cancel.cancelled() => return,
238                        };
239                        let ExchangeEnvelope { exchange, reply_tx } = envelope;
240                        let pipe_ref = Arc::clone(&pipeline);
241                        let cancel = pipeline_cancel.clone();
242                        let drain_clone = Arc::clone(&drain_in_flight);
243                        tokio::spawn(async move {
244                            // Permit owned by this task — released on completion (RAII).
245                            let _permit = permit;
246                            let _drain_guard = DrainGuard::new(drain_clone);
247
248                            // Load current pipeline from ArcSwap
249                            let mut pipe = pipe_ref.load().processor.clone_inner();
250
251                            // Wait for service ready with circuit breaker backoff
252                            if let Err(e) = ready_with_backoff(&mut pipe, &cancel).await {
253                                if let Some(tx) = reply_tx {
254                                    let _ = tx.send(Err(e));
255                                }
256                                return;
257                            }
258
259                            // B1: scope CANCEL_TOKEN so run_steps can check
260                            // cancellation between steps.
261                            let result = CANCEL_TOKEN
262                                .scope(cancel, async move { pipe.call(exchange).await })
263                                .await;
264                            if let Some(tx) = reply_tx {
265                                let _ = tx.send(result);
266                            } else if let Err(ref e) = result {
267                                // log-policy: system-broken
268                                error!("Pipeline error: {e}");
269                            }
270                        });
271                    }
272                })
273            }
274            // Forward-compat: an unknown future variant is treated as
275            // Sequential — the safe, simplest pipeline topology. A consumer
276            // that needs Concurrent semantics for a future variant must
277            // override the route's `?concurrent=` setting explicitly so the
278            // operator (not the wildcard) chooses the topology.
279            _ => {
280                tokio::spawn(async move {
281                    loop {
282                        // Use select! to exit promptly on cancellation even when idle
283                        let envelope = tokio::select! {
284                            envelope = rx.recv() => match envelope {
285                                Some(e) => e,
286                                None => return, // Channel closed
287                            },
288                            _ = pipeline_cancel.cancelled() => {
289                                // Cancellation requested - exit gracefully
290                                return;
291                            }
292                        };
293                        let ExchangeEnvelope { exchange, reply_tx } = envelope;
294
295                        // Load current pipeline from ArcSwap (picks up hot-reloaded pipelines)
296                        let mut pipeline = pipeline.load().processor.clone_inner();
297
298                        if let Err(e) = ready_with_backoff(&mut pipeline, &pipeline_cancel).await {
299                            if let Some(tx) = reply_tx {
300                                let _ = tx.send(Err(e));
301                            }
302                            return;
303                        }
304
305                        // B1: scope CANCEL_TOKEN so run_steps can check cancellation
306                        // between steps. Per-start task-local — child token expires
307                        // when this pipeline task exits; the next start re-scopes a
308                        // fresh one (avoids the lifecycle bug where a compiled-in
309                        // child token stays cancelled after stop→restart).
310                        let cancel = pipeline_cancel.clone();
311                        let _drain_guard = DrainGuard::new(Arc::clone(&drain_in_flight));
312                        let result = CANCEL_TOKEN
313                            .scope(cancel, async move { pipeline.call(exchange).await })
314                            .await;
315                        if let Some(tx) = reply_tx {
316                            let _ = tx.send(result);
317                        } else if let Err(ref e) = result {
318                            // log-policy: system-broken
319                            error!("Pipeline error: {e}");
320                        }
321                    }
322                })
323            }
324        };
325        #[cfg(test)]
326        emit_start_route_event("pipeline_spawned");
327
328        // Start consumer after pipeline task is spawned to minimize the chance of
329        // fire-and-forget events being produced before the pipeline loop is active.
330        let (consumer_handle, startup_rx) = consumer_management::spawn_consumer_task(
331            route_id.to_string(),
332            consumer,
333            consumer_ctx,
334            crash_notifier,
335            runtime_for_consumer,
336            false,
337        );
338        #[cfg(test)]
339        emit_start_route_event("consumer_spawned");
340
341        // rc-w1u9: await consumer startup handshake before returning. For
342        // Immediate consumers this is a no-op (pre-resolved receiver); for
343        // Explicit consumers (HTTP, WebSocket) it propagates bind failures as
344        // proper startup errors instead of silent background logs.
345        match consumer_management::await_consumer_startup(startup_rx, "startup").await {
346            Ok(()) => {}
347            Err(e) => {
348                // rc-kh7c: abort the orphaned consumer task and cancel the
349                // pipeline so neither runs detached after start_route returns
350                // Err. Dropping a JoinHandle detaches the task (Tokio
351                // contract); abort() forces termination. The pipeline task
352                // would eventually self-clean via rx-drop, but explicit
353                // cancellation makes it immediate.
354                consumer_handle.abort();
355                pipeline_cancel_for_cleanup.cancel();
356                // Cancel the consumer's cancel token so child tasks spawned
357                // by consumer.start() that observe ctx.cancelled() also stop.
358                consumer_cancel.cancel();
359                rollback_started(route_id, &lifecycle_handles).await;
360                return Err(e);
361            }
362        }
363
364        // Store handles and update status
365        let managed = self
366            .routes
367            .get_mut(route_id)
368            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
369        managed.consumer_handle = Some(consumer_handle);
370        managed.pipeline_handle = Some(pipeline_handle);
371        managed.channel_sender = Some(tx_for_storage);
372
373        info!(route_id = %route_id, "Route started");
374        self.health_registry().mark_route_started(route_id);
375        Ok(())
376    }
377
378    async fn stop_route(&mut self, route_id: &str) -> Result<(), CamelError> {
379        self.stop_route_internal(route_id).await?;
380        self.health_registry().mark_route_stopped(route_id);
381        Ok(())
382    }
383
384    async fn restart_route(&mut self, route_id: &str) -> Result<(), CamelError> {
385        self.stop_route(route_id).await?;
386        tokio::time::sleep(Duration::from_millis(100)).await;
387        self.start_route(route_id).await
388    }
389
390    async fn suspend_route(&mut self, route_id: &str) -> Result<(), CamelError> {
391        // Check route exists and state.
392        let managed = self
393            .routes
394            .get_mut(route_id)
395            .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
396
397        let consumer_running = handle_is_running(&managed.consumer_handle);
398        let pipeline_running = handle_is_running(&managed.pipeline_handle);
399
400        // Can only suspend from active started state.
401        if !consumer_running || !pipeline_running {
402            return Err(CamelError::RouteError(format!(
403                "Cannot suspend route '{}' with execution lifecycle {}",
404                route_id,
405                inferred_lifecycle_label(managed)
406            )));
407        }
408
409        info!(route_id = %route_id, "Suspending route (consumer only, keeping pipeline)");
410
411        // Cancel consumer token only (keep pipeline running)
412        let managed = self
413            .routes
414            .get_mut(route_id)
415            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
416        managed.consumer_cancel_token.cancel();
417
418        // Take and join consumer handle
419        let managed = self
420            .routes
421            .get_mut(route_id)
422            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
423        let consumer_handle = managed.consumer_handle.take();
424
425        // Wait for consumer task to complete with timeout
426        let timeout_result = tokio::time::timeout(DEFAULT_SHUTDOWN_TIMEOUT, async {
427            if let Some(handle) = consumer_handle {
428                let _ = handle.await;
429            }
430        })
431        .await;
432
433        if timeout_result.is_err() {
434            warn!(route_id = %route_id, "Consumer shutdown timed out during suspend");
435        }
436
437        // Get the managed route again (can't hold across await)
438        let managed = self
439            .routes
440            .get_mut(route_id)
441            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
442
443        // Create fresh cancellation token for consumer (for resume)
444        managed.consumer_cancel_token = CancellationToken::new();
445
446        info!(route_id = %route_id, "Route suspended (pipeline still running)");
447        self.health_registry().mark_route_stopped(route_id);
448        Ok(())
449    }
450
451    async fn resume_route(&mut self, route_id: &str) -> Result<(), CamelError> {
452        // Check route exists and is Suspended-equivalent execution state.
453        let managed = self
454            .routes
455            .get(route_id)
456            .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
457
458        let consumer_running = handle_is_running(&managed.consumer_handle);
459        let pipeline_running = handle_is_running(&managed.pipeline_handle);
460        if consumer_running || !pipeline_running {
461            return Err(CamelError::RouteError(format!(
462                "Cannot resume route '{}' with execution lifecycle {} (expected Suspended)",
463                route_id,
464                inferred_lifecycle_label(managed)
465            )));
466        }
467
468        // Get the stored channel sender (must exist for a suspended route)
469        let sender = managed.channel_sender.clone().ok_or_else(|| {
470            CamelError::RouteError("Suspended route has no channel sender".into())
471        })?;
472
473        // Get from_uri and concurrency for creating new consumer
474        let from_uri = managed.from_uri.clone();
475
476        info!(route_id = %route_id, "Resuming route (spawning consumer only)");
477
478        let consumer_component_ctx = Arc::new(ControllerComponentContext::new(
479            Arc::clone(&self.registry),
480            Arc::clone(&self.languages),
481            self.tracer_metrics
482                .clone()
483                .unwrap_or_else(|| Arc::new(NoOpMetrics)),
484            Arc::clone(&self.platform_service),
485            self.health_registry(),
486            Some(route_id.to_string()),
487        ));
488        let consumer_rt: Arc<dyn camel_component_api::RuntimeObservability> =
489            Arc::clone(&consumer_component_ctx) as Arc<_>;
490        let (mut consumer, _) = consumer_management::create_route_consumer(
491            consumer_rt,
492            &self.registry,
493            &from_uri,
494            consumer_component_ctx.as_ref(),
495        )?;
496
497        // Wire security context before spawning consumer
498        let managed = self
499            .routes
500            .get(route_id)
501            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
502        if let (Some(sp_config), Some(authenticator)) = (
503            managed.compiled.security_policy.as_ref(),
504            managed.compiled.security_authenticator.as_ref(),
505        ) {
506            use camel_component_api::SecurityContext;
507            let sec_ctx =
508                SecurityContext::from_arc(Arc::clone(&sp_config.policy), Arc::clone(authenticator))
509                    .with_credential_sources(sp_config.credential_sources.clone());
510            consumer.set_security_context(sec_ctx);
511        }
512
513        // Get the managed route for mutation
514        let managed = self
515            .routes
516            .get_mut(route_id)
517            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
518
519        // Create child token for consumer lifecycle
520        let consumer_cancel = managed.consumer_cancel_token.child_token();
521
522        let crash_notifier = self.crash_notifier.clone();
523        let runtime_for_consumer = self.runtime.clone();
524
525        // Create ConsumerContext with the stored sender
526        let consumer_ctx =
527            ConsumerContext::new(sender, consumer_cancel.clone(), route_id.to_string());
528
529        // Spawn consumer task
530        let (consumer_handle, startup_rx) = consumer_management::spawn_consumer_task(
531            route_id.to_string(),
532            consumer,
533            consumer_ctx,
534            crash_notifier,
535            runtime_for_consumer,
536            true,
537        );
538
539        // rc-w1u9: await consumer startup handshake on resume too — bind
540        // failures during resume must surface as resume errors.
541        consumer_management::await_consumer_startup(startup_rx, "resume").await?;
542
543        // Store consumer handle and update status
544        let managed = self
545            .routes
546            .get_mut(route_id)
547            .expect("invariant: route must exist after prior existence check"); // allow-unwrap
548        managed.consumer_handle = Some(consumer_handle);
549
550        info!(route_id = %route_id, "Route resumed");
551        self.health_registry().mark_route_started(route_id);
552        Ok(())
553    }
554
555    async fn start_all_routes(&mut self) -> Result<(), CamelError> {
556        // Only start routes where auto_startup() == true
557        // Sort by startup_order() ascending before starting
558        let route_ids: Vec<String> = {
559            let pairs = self.routes.auto_startup_sorted();
560            pairs.into_iter().map(|(id, _)| id).collect()
561        };
562
563        info!("Starting {} auto-startup routes", route_ids.len());
564
565        // Collect errors but continue starting remaining routes
566        let mut errors: Vec<String> = Vec::new();
567        for route_id in route_ids {
568            if let Err(e) = self.start_route(&route_id).await {
569                errors.push(format!("Route '{}': {}", route_id, e));
570            }
571        }
572
573        if !errors.is_empty() {
574            return Err(CamelError::RouteError(format!(
575                "Failed to start routes: {}",
576                errors.join(", ")
577            )));
578        }
579
580        info!("All auto-startup routes started");
581        Ok(())
582    }
583
584    async fn stop_all_routes(&mut self) -> Result<(), CamelError> {
585        // Sort by startup_order descending (reverse order)
586        let route_ids: Vec<String> = {
587            let pairs = self.routes.shutdown_sorted();
588            pairs.into_iter().map(|(id, _)| id).collect()
589        };
590
591        info!("Stopping {} routes", route_ids.len());
592
593        for route_id in route_ids {
594            let _ = self.stop_route(&route_id).await;
595        }
596
597        info!("All routes stopped");
598        Ok(())
599    }
600}