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