Skip to main content

camel_core/
context.rs

1use std::any::{Any, TypeId};
2use std::collections::HashMap;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5use tokio::time::timeout;
6use tokio_util::sync::CancellationToken;
7use tracing::{info, warn};
8
9#[cfg(test)]
10use camel_api::StepLifecycle;
11use camel_api::error_handler::ErrorHandlerConfig;
12use camel_api::{
13    CamelError, FunctionInvoker, HealthReport, Lifecycle, MetricsCollector, PlatformIdentity,
14    PlatformService, ReadinessGate, RouteTemplateSpec, RuntimeCommandBus, RuntimeQueryBus,
15    TemplateInstanceRecord,
16};
17use camel_component_api::{Component, ComponentContext, ComponentRegistrar};
18use camel_language_api::Language;
19
20use crate::health_registry::HealthCheckRegistry;
21use crate::lifecycle::adapters::controller_actor::RouteControllerHandle;
22use crate::lifecycle::adapters::route_controller::SharedLanguageRegistry;
23use crate::lifecycle::application::route_definition::RouteDefinition;
24use crate::lifecycle::application::runtime_bus::RuntimeBus;
25use crate::lifecycle::domain::LanguageRegistryError;
26use crate::registry::RegistryError;
27use crate::shared::components::domain::Registry;
28use crate::shared::observability::domain::TracerConfig;
29use crate::startup_validation::{ConfigCheck, run_startup_validation};
30use crate::template::TemplateRegistry;
31
32static CONTEXT_COMMAND_SEQ: AtomicU64 = AtomicU64::new(0);
33
34pub use crate::context_builder::CamelContextBuilder;
35
36/// The CamelContext is the runtime engine that manages components, routes, and their lifecycle.
37///
38/// # Lifecycle
39///
40/// A `CamelContext` is single-use: call [`start()`](Self::start) once to launch routes,
41/// then [`stop()`](Self::stop) or [`abort()`](Self::abort) to shut down. Restarting a
42/// stopped context is not supported — create a new instance instead.
43pub struct CamelContext {
44    registry: Arc<std::sync::Mutex<Registry>>,
45    route_controller: RouteControllerHandle,
46    _actor_join: tokio::task::JoinHandle<()>,
47    supervision_join: Option<tokio::task::JoinHandle<()>>,
48    runtime: Arc<RuntimeBus>,
49    cancel_token: CancellationToken,
50    metrics: Arc<dyn MetricsCollector>,
51    // Platform ports
52    platform_service: Arc<dyn PlatformService>,
53    languages: SharedLanguageRegistry,
54    shutdown_timeout: std::time::Duration,
55    services: Vec<Box<dyn Lifecycle>>,
56    health_registry: Arc<HealthCheckRegistry>,
57    component_configs: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
58    function_invoker: Option<Arc<dyn FunctionInvoker>>,
59    template_registry: Arc<TemplateRegistry>,
60    idempotent_repositories: crate::registry::SharedIdempotentRegistry,
61    claim_check_repositories: crate::registry::SharedClaimCheckRegistry,
62    /// Fail-closed startup validation registry (ADR-0033). Checks are drained
63    /// and executed synchronously at the head of [`start()`](Self::start) before
64    /// any route consumer is started.
65    startup_checks: Vec<Box<dyn ConfigCheck>>,
66}
67
68/// Parts bag used by [`CamelContextBuilder::build`] to construct a [`CamelContext`]
69/// without accessing private fields from a sibling module.
70pub(crate) struct FromParts {
71    pub(crate) registry: Arc<std::sync::Mutex<Registry>>,
72    pub(crate) route_controller: RouteControllerHandle,
73    pub(crate) _actor_join: tokio::task::JoinHandle<()>,
74    pub(crate) supervision_join: Option<tokio::task::JoinHandle<()>>,
75    pub(crate) runtime: Arc<RuntimeBus>,
76    pub(crate) cancel_token: CancellationToken,
77    pub(crate) metrics: Arc<dyn MetricsCollector>,
78    pub(crate) platform_service: Arc<dyn PlatformService>,
79    pub(crate) languages: SharedLanguageRegistry,
80    pub(crate) shutdown_timeout: std::time::Duration,
81    pub(crate) services: Vec<Box<dyn Lifecycle>>,
82    pub(crate) health_registry: Arc<HealthCheckRegistry>,
83    pub(crate) component_configs: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
84    pub(crate) function_invoker: Option<Arc<dyn FunctionInvoker>>,
85    pub(crate) template_registry: Arc<TemplateRegistry>,
86    pub(crate) idempotent_repositories: crate::registry::SharedIdempotentRegistry,
87    pub(crate) claim_check_repositories: crate::registry::SharedClaimCheckRegistry,
88    pub(crate) startup_checks: Vec<Box<dyn ConfigCheck>>,
89}
90
91impl CamelContext {
92    pub(crate) fn from_parts(parts: FromParts) -> Self {
93        Self {
94            registry: parts.registry,
95            route_controller: parts.route_controller,
96            _actor_join: parts._actor_join,
97            supervision_join: parts.supervision_join,
98            runtime: parts.runtime,
99            cancel_token: parts.cancel_token,
100            metrics: parts.metrics,
101            platform_service: parts.platform_service,
102            languages: parts.languages,
103            shutdown_timeout: parts.shutdown_timeout,
104            services: parts.services,
105            health_registry: parts.health_registry,
106            component_configs: parts.component_configs,
107            function_invoker: parts.function_invoker,
108            template_registry: parts.template_registry,
109            idempotent_repositories: parts.idempotent_repositories,
110            claim_check_repositories: parts.claim_check_repositories,
111            startup_checks: parts.startup_checks,
112        }
113    }
114}
115
116/// Opaque handle for runtime side-effect execution operations.
117///
118/// This intentionally does not expose direct lifecycle mutation APIs to callers.
119#[derive(Clone)]
120pub struct RuntimeExecutionHandle {
121    pub(crate) controller: RouteControllerHandle,
122    pub(crate) runtime: Arc<RuntimeBus>,
123    pub(crate) function_invoker: Option<Arc<dyn FunctionInvoker>>,
124    /// Lifecycle handles to inject into the compiled pipeline during
125    /// `apply_swap`.  Used in tests to simulate lifecycle-bearing routes
126    /// (e.g. resequencer).  Always `None` in production.
127    #[cfg(test)]
128    #[allow(clippy::type_complexity)]
129    pub(crate) test_lifecycle_inject: Arc<std::sync::Mutex<Option<Vec<Arc<dyn StepLifecycle>>>>>,
130}
131
132impl RuntimeExecutionHandle {
133    pub(crate) async fn add_route_definition(
134        &self,
135        definition: RouteDefinition,
136    ) -> Result<(), CamelError> {
137        use crate::lifecycle::ports::RouteRegistrationPort;
138        self.runtime
139            .register_route(definition)
140            .await
141            .map_err(Into::into)
142    }
143
144    /// Compile a route definition into a bare BoxProcessor (no lifecycle).
145    /// Kept for tests; hot-reload uses the lifecycle-preserving variant instead.
146    #[allow(dead_code)]
147    pub(crate) async fn compile_route_definition(
148        &self,
149        definition: RouteDefinition,
150    ) -> Result<camel_api::BoxProcessor, CamelError> {
151        self.controller.compile_route_definition(definition).await
152    }
153
154    #[allow(dead_code)] // kept for potential future hot-reload paths
155    pub(crate) async fn compile_route_definition_with_generation(
156        &self,
157        definition: RouteDefinition,
158        generation: u64,
159    ) -> Result<camel_api::BoxProcessor, CamelError> {
160        self.controller
161            .compile_route_definition_with_generation(definition, generation)
162            .await
163    }
164
165    pub(crate) async fn compile_route_definition_pipeline(
166        &self,
167        definition: RouteDefinition,
168        generation: u64,
169    ) -> Result<crate::lifecycle::adapters::route_helpers::CompiledPipeline, CamelError> {
170        self.controller
171            .compile_route_definition_pipeline(definition, generation)
172            .await
173    }
174
175    /// Compile without function generation, returning full CompiledPipeline.
176    /// Oracle Fix 1: stateless hot-reload path preserves lifecycle handles.
177    pub(crate) async fn compile_route_definition_dry_pipeline(
178        &self,
179        definition: RouteDefinition,
180    ) -> Result<crate::lifecycle::adapters::route_helpers::CompiledPipeline, CamelError> {
181        self.controller
182            .compile_route_definition_dry_pipeline(definition)
183            .await
184    }
185
186    pub(crate) async fn prepare_route_definition_with_generation(
187        &self,
188        definition: RouteDefinition,
189        generation: u64,
190    ) -> Result<crate::lifecycle::adapters::route_controller::PreparedRoute, CamelError> {
191        self.controller
192            .prepare_route_definition_with_generation(definition, generation)
193            .await
194    }
195
196    pub(crate) async fn insert_prepared_route(
197        &self,
198        prepared: crate::lifecycle::adapters::route_controller::PreparedRoute,
199    ) -> Result<(), CamelError> {
200        self.controller.insert_prepared_route(prepared).await
201    }
202
203    pub(crate) async fn remove_route_preserving_functions(
204        &self,
205        route_id: String,
206    ) -> Result<(), CamelError> {
207        self.controller
208            .remove_route_preserving_functions(route_id)
209            .await
210    }
211
212    pub(crate) async fn register_route_aggregate(
213        &self,
214        route_id: String,
215    ) -> Result<(), CamelError> {
216        self.runtime.register_aggregate_only(route_id).await
217    }
218
219    pub(crate) async fn swap_route_pipeline(
220        &self,
221        route_id: &str,
222        pipeline: camel_api::BoxProcessor,
223    ) -> Result<(), CamelError> {
224        self.controller.swap_pipeline(route_id, pipeline).await
225    }
226
227    /// Stop the route via the reload path (graceful lifecycle drain).
228    pub(crate) async fn stop_route_reload(&self, route_id: &str) -> Result<(), CamelError> {
229        self.controller.stop_route_reload(route_id).await
230    }
231
232    /// Start the route via the reload path (re-create consumer).
233    pub(crate) async fn start_route_reload(&self, route_id: &str) -> Result<(), CamelError> {
234        self.controller.start_route_reload(route_id).await
235    }
236
237    /// Raw pipeline swap — bypasses the lifecycle/aggregate rejection check.
238    /// Only safe after the route has been stopped (Restart path).
239    pub(crate) async fn swap_route_pipeline_raw(
240        &self,
241        route_id: &str,
242        pipeline: camel_api::BoxProcessor,
243        lifecycle: Vec<Arc<dyn camel_api::StepLifecycle>>,
244    ) -> Result<(), CamelError> {
245        self.controller
246            .swap_pipeline_raw(route_id, pipeline, lifecycle)
247            .await
248    }
249
250    pub(crate) async fn execute_runtime_command(
251        &self,
252        cmd: camel_api::RuntimeCommand,
253    ) -> Result<camel_api::RuntimeCommandResult, CamelError> {
254        self.runtime.execute(cmd).await
255    }
256
257    pub(crate) async fn runtime_route_status(
258        &self,
259        route_id: &str,
260    ) -> Result<Option<String>, CamelError> {
261        match self
262            .runtime
263            .ask(camel_api::RuntimeQuery::GetRouteStatus {
264                route_id: route_id.to_string(),
265            })
266            .await
267        {
268            Ok(camel_api::RuntimeQueryResult::RouteStatus { status, .. }) => Ok(Some(status)),
269            Ok(_) => Err(CamelError::RouteError(
270                "unexpected runtime query response for route status".to_string(),
271            )),
272            Err(CamelError::RouteError(msg)) if msg.contains("not found") => Ok(None),
273            Err(err) => Err(err),
274        }
275    }
276
277    pub(crate) async fn runtime_route_ids(&self) -> Result<Vec<String>, CamelError> {
278        match self.runtime.ask(camel_api::RuntimeQuery::ListRoutes).await {
279            Ok(camel_api::RuntimeQueryResult::Routes { route_ids }) => Ok(route_ids),
280            Ok(_) => Err(CamelError::RouteError(
281                "unexpected runtime query response for route listing".to_string(),
282            )),
283            Err(err) => Err(err),
284        }
285    }
286
287    pub(crate) async fn route_source_hash(&self, route_id: &str) -> Option<u64> {
288        self.controller.route_source_hash(route_id).await
289    }
290
291    pub(crate) async fn in_flight_count(&self, route_id: &str) -> Result<u64, CamelError> {
292        if !self.controller.route_exists(route_id).await? {
293            return Err(CamelError::RouteError(format!(
294                "Route '{}' not found",
295                route_id
296            )));
297        }
298        Ok(self
299            .controller
300            .in_flight_count(route_id)
301            .await?
302            .unwrap_or(0))
303    }
304
305    /// Check whether the running route has lifecycle-bearing steps.
306    pub(crate) async fn route_has_lifecycle(&self, route_id: &str) -> bool {
307        self.controller
308            .route_has_lifecycle(route_id)
309            .await
310            .unwrap_or(false)
311    }
312
313    pub(crate) fn function_invoker(&self) -> Option<Arc<dyn FunctionInvoker>> {
314        self.function_invoker.clone()
315    }
316
317    #[cfg(test)]
318    pub(crate) async fn force_start_route_for_test(
319        &self,
320        route_id: &str,
321    ) -> Result<(), CamelError> {
322        self.controller.start_route(route_id).await
323    }
324
325    pub async fn controller_route_count_for_test(&self) -> usize {
326        self.controller.route_count().await.unwrap_or(0)
327    }
328}
329
330impl CamelContext {
331    pub fn builder() -> CamelContextBuilder {
332        CamelContextBuilder::new()
333    }
334
335    /// Set a global error handler applied to all routes without a per-route handler.
336    pub async fn set_error_handler(&mut self, config: ErrorHandlerConfig) {
337        let _ = self.route_controller.set_error_handler(config).await;
338    }
339
340    /// Enable or disable tracing globally.
341    pub async fn set_tracing(&mut self, enabled: bool) {
342        let _ = self
343            .route_controller
344            .set_tracer_config(TracerConfig {
345                enabled,
346                ..Default::default()
347            })
348            .await;
349    }
350
351    /// Configure tracing with full config.
352    pub async fn set_tracer_config(&mut self, config: TracerConfig) {
353        // Inject metrics collector if not already set
354        let config = if config.metrics_collector.is_none() {
355            TracerConfig {
356                metrics_collector: Some(Arc::clone(&self.metrics)),
357                ..config
358            }
359        } else {
360            config
361        };
362
363        let _ = self.route_controller.set_tracer_config(config).await;
364    }
365
366    /// Builder-style: enable tracing with default config.
367    pub async fn with_tracing(mut self) -> Self {
368        self.set_tracing(true).await;
369        self
370    }
371
372    /// Builder-style: configure tracing with custom config.
373    /// Note: tracing subscriber initialization (stdout/file output) is handled
374    /// separately via init_tracing_subscriber (called in camel-config bridge).
375    pub async fn with_tracer_config(mut self, config: TracerConfig) -> Self {
376        self.set_tracer_config(config).await;
377        self
378    }
379
380    /// Register a lifecycle service (Apache Camel: addService pattern)
381    ///
382    /// For services exposing `as_function_invoker()`, the invoker is propagated
383    /// to the route controller so that subsequent route definitions with function
384    /// steps work correctly.
385    ///
386    /// Prefer [`CamelContextBuilder::with_lifecycle`] when possible, which wires
387    /// the invoker at build time before any routes are added.
388    pub fn with_lifecycle<L: Lifecycle + 'static>(mut self, service: L) -> Self {
389        if let Some(collector) = service.as_metrics_collector() {
390            self.metrics = collector;
391        }
392        if let Some(invoker) = service.as_function_invoker() {
393            self.function_invoker = Some(invoker.clone());
394            if let Err(e) = self.route_controller.try_set_function_invoker(invoker) {
395                tracing::warn!("Failed to propagate function invoker to route controller: {e}");
396            }
397        }
398
399        self.services.push(Box::new(service));
400        self
401    }
402
403    /// Register a component with this context.
404    pub fn register_component<C: Component + 'static>(&mut self, component: C) {
405        info!(scheme = component.scheme(), "Registering component");
406        self.registry
407            .lock()
408            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
409            .register(Arc::new(component));
410    }
411
412    /// Register a startup `ConfigCheck` to be evaluated at the head of
413    /// [`start()`](Self::start). Established by ADR-0033.
414    ///
415    /// Checks are drained and executed synchronously before any route consumer
416    /// is started. If any check returns `Err`, `start()` fails closed with
417    /// `CamelError::Config(_)` and no route is started. The check list is
418    /// consumed (moved) during `start()` so this method may be called multiple
419    /// times to register an arbitrary number of checks.
420    pub fn add_startup_check(&mut self, check: Box<dyn ConfigCheck>) {
421        self.startup_checks.push(check);
422    }
423
424    /// Register a language with this context, keyed by name.
425    ///
426    /// Returns `Err(LanguageRegistryError::AlreadyRegistered)` if a language
427    /// with the same name is already registered. Use
428    /// [`resolve_language`](Self::resolve_language) to check before
429    /// registering, or choose a distinct name.
430    pub fn register_language(
431        &mut self,
432        name: impl Into<String>,
433        lang: Box<dyn Language>,
434    ) -> Result<(), LanguageRegistryError> {
435        let name = name.into();
436        let mut languages = self
437            .languages
438            .lock()
439            .expect("mutex poisoned: another thread panicked while holding this lock"); // allow-unwrap
440        if languages.contains_key(&name) {
441            return Err(LanguageRegistryError::AlreadyRegistered { name });
442        }
443        languages.insert(name, Arc::from(lang));
444        Ok(())
445    }
446
447    /// Resolve a language by name. Returns `None` if not registered.
448    pub fn resolve_language(&self, name: &str) -> Option<Arc<dyn Language>> {
449        let languages = self
450            .languages
451            .lock()
452            .expect("mutex poisoned: another thread panicked while holding this lock"); // allow-unwrap
453        languages.get(name).cloned()
454    }
455
456    /// Add a route definition to this context.
457    ///
458    /// The route must have an ID. Steps are resolved immediately using registered components.
459    pub async fn add_route_definition(
460        &self,
461        definition: RouteDefinition,
462    ) -> Result<(), CamelError> {
463        use crate::lifecycle::ports::RouteRegistrationPort;
464        info!(
465            from = definition.from_uri(),
466            route_id = %definition.route_id(),
467            "Adding route definition"
468        );
469        self.runtime
470            .register_route(definition)
471            .await
472            .map_err(Into::into)
473    }
474
475    fn next_context_command_id(op: &str, route_id: &str) -> String {
476        let seq = CONTEXT_COMMAND_SEQ.fetch_add(1, Ordering::Relaxed);
477        format!("context:{op}:{route_id}:{seq}")
478    }
479
480    /// Access the component registry.
481    pub fn registry(&self) -> std::sync::MutexGuard<'_, Registry> {
482        self.registry
483            .lock()
484            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
485    }
486
487    /// Access the shared component registry Arc.
488    pub fn registry_arc(&self) -> Arc<std::sync::Mutex<Registry>> {
489        Arc::clone(&self.registry)
490    }
491
492    /// Get runtime execution handle for file-watcher integrations.
493    pub fn runtime_execution_handle(&self) -> RuntimeExecutionHandle {
494        RuntimeExecutionHandle {
495            controller: self.route_controller.clone(),
496            runtime: Arc::clone(&self.runtime),
497            function_invoker: self.function_invoker.clone(),
498            #[cfg(test)]
499            test_lifecycle_inject: Arc::new(std::sync::Mutex::new(None)),
500        }
501    }
502
503    /// Get the metrics collector.
504    pub fn metrics(&self) -> Arc<dyn MetricsCollector> {
505        Arc::clone(&self.metrics)
506    }
507
508    /// Get the platform service.
509    pub fn platform_service(&self) -> Arc<dyn PlatformService> {
510        Arc::clone(&self.platform_service)
511    }
512
513    /// Get the readiness gate port.
514    pub fn readiness_gate(&self) -> Arc<dyn ReadinessGate> {
515        self.platform_service.readiness_gate()
516    }
517
518    /// Get the platform identity.
519    pub fn platform_identity(&self) -> PlatformIdentity {
520        self.platform_service.identity()
521    }
522
523    /// Get the leadership service port.
524    pub fn leadership(&self) -> Arc<dyn camel_api::LeadershipService> {
525        self.platform_service.leadership()
526    }
527
528    /// Get runtime command/query bus handle.
529    pub fn runtime(&self) -> Arc<dyn camel_api::RuntimeHandle> {
530        self.runtime.clone()
531    }
532
533    /// Build a producer context wired to this runtime.
534    pub fn producer_context(&self) -> camel_api::ProducerContext {
535        camel_api::ProducerContext::new().with_runtime(self.runtime())
536    }
537
538    /// Query route status via runtime read-model.
539    pub async fn runtime_route_status(&self, route_id: &str) -> Result<Option<String>, CamelError> {
540        match self
541            .runtime()
542            .ask(camel_api::RuntimeQuery::GetRouteStatus {
543                route_id: route_id.to_string(),
544            })
545            .await
546        {
547            Ok(camel_api::RuntimeQueryResult::RouteStatus { status, .. }) => Ok(Some(status)),
548            Ok(_) => Err(CamelError::RouteError(
549                "unexpected runtime query response for route status".to_string(),
550            )),
551            Err(CamelError::RouteError(msg)) if msg.contains("not found") => Ok(None),
552            Err(err) => Err(err),
553        }
554    }
555
556    /// Start all routes. Each route's consumer will begin producing exchanges.
557    ///
558    /// Only routes with `auto_startup == true` will be started, in order of their
559    /// `startup_order` (lower values start first).
560    pub async fn start(&mut self) -> Result<(), CamelError> {
561        info!("Starting CamelContext");
562
563        // Start lifecycle services first
564        for (i, service) in self.services.iter_mut().enumerate() {
565            info!("Starting service: {}", service.name());
566            if let Err(e) = service.start().await {
567                // Rollback: stop already started services in reverse order
568                warn!(
569                    "Service {} failed to start, rolling back {} services",
570                    service.name(),
571                    i
572                );
573                for j in (0..i).rev() {
574                    if let Err(rollback_err) = self.services[j].stop().await {
575                        warn!(
576                            "Failed to stop service {} during rollback: {}",
577                            self.services[j].name(),
578                            rollback_err
579                        );
580                    }
581                }
582                return Err(e);
583            }
584        }
585
586        // ADR-0033: fail-closed startup validation. Drain the registered
587        // ConfigCheck list and run every check synchronously. If any check
588        // returns Err, refuse to start the runtime — no route consumer is
589        // started, no reconciliation runs. Drains the registry so a second
590        // call to start() (currently not supported) would not re-run checks.
591        let checks = std::mem::take(&mut self.startup_checks);
592        if let Err(e) = run_startup_validation(checks) {
593            warn!("Startup validation failed: {e}");
594            return Err(e);
595        }
596
597        // H8: boot reconciliation — fail routes stuck in transient state
598        // (Starting/Stopping) from a previous run before auto_startup runs.
599        self.runtime
600            .reconcile_transient_states()
601            .await
602            .map_err(|e| CamelError::RouteError(format!("boot reconciliation failed: {e}")))?;
603
604        // Then start routes via runtime command bus (aggregate-first),
605        // preserving route controller startup ordering metadata.
606        let route_ids = self.route_controller.auto_startup_route_ids().await?;
607        for route_id in route_ids {
608            self.runtime
609                .execute(camel_api::RuntimeCommand::StartRoute {
610                    route_id: route_id.clone(),
611                    command_id: Self::next_context_command_id("start", &route_id),
612                    causation_id: None,
613                })
614                .await?;
615        }
616
617        info!("CamelContext started");
618        Ok(())
619    }
620
621    /// Graceful shutdown with default 30-second timeout.
622    pub async fn stop(&mut self) -> Result<(), CamelError> {
623        self.stop_timeout(self.shutdown_timeout).await
624    }
625
626    /// Graceful shutdown with custom timeout.
627    ///
628    /// Note: The timeout parameter is currently not propagated to the
629    /// RouteController's per-route shutdown timeout. The RouteController
630    /// uses a hardcoded 5-second default (`DEFAULT_SHUTDOWN_TIMEOUT`).
631    /// Full propagation is planned for a future version.
632    pub async fn stop_timeout(&mut self, _timeout: std::time::Duration) -> Result<(), CamelError> {
633        info!("Stopping CamelContext");
634
635        // Signal cancellation (for any legacy code that might use it)
636        self.cancel_token.cancel();
637        if let Some(join) = self.supervision_join.take() {
638            join.abort();
639        }
640
641        // Stop all routes via runtime command bus (aggregate-first),
642        // preserving route controller shutdown ordering metadata.
643        let route_ids = self.route_controller.shutdown_route_ids().await?;
644        for route_id in route_ids {
645            if let Err(err) = self
646                .runtime
647                .execute(camel_api::RuntimeCommand::StopRoute {
648                    route_id: route_id.clone(),
649                    command_id: Self::next_context_command_id("stop", &route_id),
650                    causation_id: None,
651                })
652                .await
653            {
654                warn!(route_id = %route_id, error = %err, "Runtime stop command failed during context shutdown");
655            }
656        }
657
658        self.health_registry.cancel_token().cancel();
659
660        // Then stop lifecycle services in reverse insertion order (LIFO)
661        // Continue stopping all services even if some fail
662        let mut first_error = None;
663        for service in self.services.iter_mut().rev() {
664            info!("Stopping service: {}", service.name());
665            if let Err(e) = service.stop().await {
666                warn!("Service {} failed to stop: {}", service.name(), e);
667                if first_error.is_none() {
668                    first_error = Some(e);
669                }
670            }
671        }
672
673        info!("CamelContext stopped");
674
675        if let Some(e) = first_error {
676            Err(e)
677        } else {
678            Ok(())
679        }
680    }
681
682    /// Get the graceful shutdown timeout used by [`stop()`](Self::stop).
683    pub fn shutdown_timeout(&self) -> std::time::Duration {
684        self.shutdown_timeout
685    }
686
687    /// Set the graceful shutdown timeout used by [`stop()`](Self::stop).
688    pub fn set_shutdown_timeout(&mut self, timeout: std::time::Duration) {
689        self.shutdown_timeout = timeout;
690    }
691
692    /// Immediate abort — kills all tasks without draining.
693    pub async fn abort(&mut self) {
694        self.cancel_token.cancel();
695        if let Some(join) = self.supervision_join.take() {
696            join.abort();
697        }
698        let route_ids = self
699            .route_controller
700            .shutdown_route_ids()
701            .await
702            .unwrap_or_default();
703        for route_id in route_ids {
704            let _ = self
705                .runtime
706                .execute(camel_api::RuntimeCommand::StopRoute {
707                    route_id: route_id.clone(),
708                    command_id: Self::next_context_command_id("abort-stop", &route_id),
709                    causation_id: None,
710                })
711                .await;
712        }
713
714        for service in self.services.iter_mut().rev() {
715            let name = service.name().to_string();
716            match timeout(std::time::Duration::from_secs(5), service.stop()).await {
717                Ok(Ok(())) => info!("Aborted service: {}", name),
718                Ok(Err(e)) => warn!("Service {} failed to stop during abort: {}", name, e),
719                Err(_) => warn!("Service {} timed out during abort (5s)", name),
720            }
721        }
722    }
723
724    /// Check health status of all registered services and lifecycle services.
725    pub async fn health_check(&self) -> HealthReport {
726        use camel_api::HealthSource;
727        self.health_report().await
728    }
729
730    pub fn health_registry(&self) -> Arc<HealthCheckRegistry> {
731        Arc::clone(&self.health_registry)
732    }
733
734    /// Store a component config. Overwrites any previously stored config of the same type.
735    pub fn set_component_config<T: 'static + Send + Sync>(&mut self, config: T) {
736        self.component_configs
737            .insert(TypeId::of::<T>(), Box::new(config));
738    }
739
740    /// Retrieve a stored component config by type. Returns None if not stored.
741    pub fn get_component_config<T: 'static + Send + Sync>(&self) -> Option<&T> {
742        self.component_configs
743            .get(&TypeId::of::<T>())
744            .and_then(|b| b.downcast_ref::<T>())
745    }
746
747    // --- Route Template Registry (data-only) ---
748
749    /// Register a route template specification.
750    ///
751    /// Returns `Err(CamelError)` if a template with the same ID is already registered.
752    pub fn add_route_template(&self, spec: RouteTemplateSpec) -> Result<(), CamelError> {
753        self.template_registry.register(spec)
754    }
755
756    /// Retrieve a route template specification by its ID.
757    pub fn get_route_template(&self, id: &str) -> Option<RouteTemplateSpec> {
758        self.template_registry.get(id)
759    }
760
761    /// Return all registered template IDs.
762    pub fn template_ids(&self) -> Vec<String> {
763        self.template_registry.template_ids()
764    }
765
766    /// Record a newly instantiated template instance.
767    pub fn record_template_instance(&self, record: TemplateInstanceRecord) {
768        self.template_registry.record_instance(record)
769    }
770
771    /// Return all instance records for a given template ID.
772    pub fn template_instances(&self, template_id: &str) -> Vec<TemplateInstanceRecord> {
773        self.template_registry.instances(template_id)
774    }
775
776    // --- Idempotent Repository Registry ---
777
778    /// Register an idempotent repository.
779    ///
780    /// Returns `Err(RegistryError::AlreadyRegistered)` if a repository with
781    /// the same name is already registered.
782    pub fn register_idempotent_repository(
783        &mut self,
784        name: impl Into<String>,
785        repo: Arc<dyn camel_api::IdempotentRepository>,
786    ) -> Result<(), RegistryError> {
787        self.idempotent_repositories.register(name, repo)
788    }
789
790    /// Retrieve an idempotent repository by name.
791    pub fn idempotent_repository(
792        &self,
793        name: &str,
794    ) -> Option<Arc<dyn camel_api::IdempotentRepository>> {
795        self.idempotent_repositories.get(name)
796    }
797
798    // --- Claim Check Repository Registry ---
799
800    /// Register a claim check repository.
801    ///
802    /// Returns `Err(RegistryError::AlreadyRegistered)` if a repository with
803    /// the same name is already registered.
804    pub fn register_claim_check_repository(
805        &mut self,
806        name: impl Into<String>,
807        repo: Arc<dyn camel_api::ClaimCheckRepository>,
808    ) -> Result<(), RegistryError> {
809        self.claim_check_repositories.register(name, repo)
810    }
811
812    /// Retrieve a claim check repository by name.
813    pub fn claim_check_repository(
814        &self,
815        name: &str,
816    ) -> Option<Arc<dyn camel_api::ClaimCheckRepository>> {
817        self.claim_check_repositories.get(name)
818    }
819}
820
821impl ComponentRegistrar for CamelContext {
822    fn register_component_dyn(&mut self, component: Arc<dyn Component>) {
823        self.registry
824            .lock()
825            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
826            .register(component);
827    }
828}
829
830impl ComponentContext for CamelContext {
831    fn resolve_component(&self, scheme: &str) -> Option<Arc<dyn Component>> {
832        self.registry.lock().ok()?.get(scheme)
833    }
834
835    fn resolve_language(&self, name: &str) -> Option<Arc<dyn Language>> {
836        self.languages.lock().ok()?.get(name).cloned()
837    }
838
839    fn metrics(&self) -> Arc<dyn MetricsCollector> {
840        Arc::clone(&self.metrics)
841    }
842
843    fn health(&self) -> Arc<dyn camel_component_api::HealthCheckRegistry> {
844        // The concrete HealthCheckRegistry struct implements the trait via
845        // the impl added in health_registry.rs.
846        Arc::clone(&self.health_registry) as Arc<dyn camel_component_api::HealthCheckRegistry>
847    }
848
849    fn platform_service(&self) -> Arc<dyn PlatformService> {
850        Arc::clone(&self.platform_service)
851    }
852
853    fn register_route_health_check(
854        &self,
855        route_id: &str,
856        check: Arc<dyn camel_api::AsyncHealthCheck>,
857    ) {
858        self.health_registry.register_for_route(route_id, check);
859    }
860
861    fn unregister_route_health_check(&self, route_id: &str) {
862        self.health_registry.unregister_for_route(route_id);
863    }
864}
865
866#[async_trait::async_trait]
867impl camel_api::HealthSource for CamelContext {
868    async fn liveness(&self) -> camel_api::HealthStatus {
869        let has_failed = self
870            .services
871            .iter()
872            .any(|s| s.status() == camel_api::ServiceStatus::Failed);
873        if has_failed {
874            camel_api::HealthStatus::Unhealthy
875        } else {
876            camel_api::HealthStatus::Healthy
877        }
878    }
879
880    async fn readiness(&self) -> camel_api::HealthStatus {
881        let has_failed = self
882            .services
883            .iter()
884            .any(|s| s.status() == camel_api::ServiceStatus::Failed);
885        if has_failed {
886            return camel_api::HealthStatus::Unhealthy;
887        }
888        let has_stopped = self
889            .services
890            .iter()
891            .any(|s| s.status() == camel_api::ServiceStatus::Stopped);
892        if has_stopped {
893            return camel_api::HealthStatus::Degraded;
894        }
895        self.health_registry.check_all().await.status
896    }
897
898    async fn health_report(&self) -> camel_api::HealthReport {
899        let mut report = self.health_registry.check_all().await;
900        let mut worst = report.status;
901        for service in &self.services {
902            let svc_status = service.status();
903            let health = match svc_status {
904                camel_api::ServiceStatus::Started => camel_api::HealthStatus::Healthy,
905                camel_api::ServiceStatus::Stopped => camel_api::HealthStatus::Degraded,
906                camel_api::ServiceStatus::Failed => camel_api::HealthStatus::Unhealthy,
907            };
908            if matches!(worst, camel_api::HealthStatus::Healthy)
909                && matches!(
910                    health,
911                    camel_api::HealthStatus::Degraded | camel_api::HealthStatus::Unhealthy
912                )
913            {
914                worst = health;
915            }
916            if matches!(worst, camel_api::HealthStatus::Degraded)
917                && matches!(health, camel_api::HealthStatus::Unhealthy)
918            {
919                worst = health;
920            }
921            report.services.push(camel_api::ServiceHealth {
922                name: service.name().to_string(),
923                status: svc_status,
924                message: None,
925            });
926        }
927        report.status = worst;
928        report
929    }
930
931    async fn startup(&self) -> camel_api::HealthStatus {
932        camel_api::HealthStatus::Healthy
933    }
934}
935
936#[cfg(test)]
937#[path = "context_tests.rs"]
938mod context_tests;