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