Skip to main content

camel_core/
context.rs

1use std::any::{Any, TypeId};
2use std::collections::HashMap;
3use std::sync::Arc;
4use tokio_util::sync::CancellationToken;
5use tracing::{debug, trace};
6
7#[cfg(test)]
8use camel_api::StepLifecycle;
9use camel_api::component_metadata::ComponentMetadata;
10use camel_api::error_handler::ErrorHandlerConfig;
11use camel_api::{
12    CamelError, FunctionInvoker, HealthReport, Lifecycle, MetricsCollector, PlatformIdentity,
13    PlatformService, ReadinessGate, RouteTemplateSpec, RuntimeCommandBus, RuntimeQueryBus,
14    TemplateInstanceRecord,
15};
16use camel_component_api::{Component, ComponentContext, ComponentRegistrar};
17use camel_language_api::Language;
18
19use crate::health_registry::HealthCheckRegistry;
20use crate::intercept::InterceptRules;
21use crate::language_registry::LanguageRegistryError;
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::registry::RegistryError;
27use crate::shared::components::domain::Registry;
28use crate::shared::observability::domain::TracerConfig;
29use crate::startup_validation::ConfigCheck;
30use crate::template::TemplateRegistry;
31
32pub use crate::context_builder::CamelContextBuilder;
33
34/// The CamelContext is the runtime engine that manages components, routes, and their lifecycle.
35///
36/// # Lifecycle
37///
38/// Call [`start()`](Self::start) to launch routes, then [`stop()`](Self::stop)
39/// or [`abort()`](Self::abort) to shut down. A stopped context can be restarted
40/// by calling `start()` again — the controller actor stays alive across stop/start
41/// cycles; only [`abort()`](Self::abort) is destructive.
42pub struct CamelContext {
43    registry: Arc<std::sync::Mutex<Registry>>,
44    route_controller: RouteControllerHandle,
45    actor_join: Option<tokio::task::JoinHandle<()>>,
46    supervision_join: Option<tokio::task::JoinHandle<()>>,
47    runtime: Arc<RuntimeBus>,
48    cancel_token: CancellationToken,
49    metrics: Arc<dyn MetricsCollector>,
50    // Platform ports
51    platform_service: Arc<dyn PlatformService>,
52    languages: SharedLanguageRegistry,
53    shutdown_timeout: std::time::Duration,
54    services: Vec<Box<dyn Lifecycle>>,
55    health_registry: Arc<HealthCheckRegistry>,
56    component_configs: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
57    function_invoker: Option<Arc<dyn FunctionInvoker>>,
58    template_registry: Arc<TemplateRegistry>,
59    idempotent_repositories: crate::registry::SharedIdempotentRegistry,
60    claim_check_repositories: crate::registry::SharedClaimCheckRegistry,
61    cache_repositories: crate::registry::SharedCacheRegistry,
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) cache_repositories: crate::registry::SharedCacheRegistry,
89    pub(crate) startup_checks: Vec<Box<dyn ConfigCheck>>,
90}
91
92impl CamelContext {
93    pub(crate) fn from_parts(parts: FromParts) -> Self {
94        Self {
95            registry: parts.registry,
96            route_controller: parts.route_controller,
97            actor_join: Some(parts._actor_join),
98            supervision_join: parts.supervision_join,
99            runtime: parts.runtime,
100            cancel_token: parts.cancel_token,
101            metrics: parts.metrics,
102            platform_service: parts.platform_service,
103            languages: parts.languages,
104            shutdown_timeout: parts.shutdown_timeout,
105            services: parts.services,
106            health_registry: parts.health_registry,
107            component_configs: parts.component_configs,
108            function_invoker: parts.function_invoker,
109            template_registry: parts.template_registry,
110            idempotent_repositories: parts.idempotent_repositories,
111            claim_check_repositories: parts.claim_check_repositories,
112            cache_repositories: parts.cache_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::application::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::domain::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::domain::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::domain::route_compilation::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::domain::route_compilation::PreparedRoute,
201    ) -> Result<(), CamelError> {
202        self.controller.insert_prepared_route(prepared).await
203    }
204
205    pub(crate) async fn discard_prepared_staging(&self, route_id: &str) -> Result<(), CamelError> {
206        self.controller.discard_prepared_staging(route_id).await
207    }
208
209    pub(crate) async fn remove_route_preserving_functions(
210        &self,
211        route_id: String,
212    ) -> Result<(), CamelError> {
213        self.controller
214            .remove_route_preserving_functions(route_id)
215            .await
216    }
217
218    pub(crate) async fn register_route_aggregate(
219        &self,
220        route_id: String,
221    ) -> Result<(), CamelError> {
222        self.runtime.register_aggregate_only(route_id).await
223    }
224
225    pub(crate) async fn swap_route_pipeline(
226        &self,
227        route_id: &str,
228        pipeline: camel_api::BoxProcessor,
229    ) -> Result<(), CamelError> {
230        self.controller.swap_pipeline(route_id, pipeline).await
231    }
232
233    /// Stop the route via the reload path (graceful lifecycle drain).
234    pub(crate) async fn stop_route_reload(&self, route_id: &str) -> Result<(), CamelError> {
235        self.controller.stop_route_reload(route_id).await
236    }
237
238    /// Start the route via the reload path (re-create consumer).
239    pub(crate) async fn start_route_reload(&self, route_id: &str) -> Result<(), CamelError> {
240        self.controller.start_route_reload(route_id).await
241    }
242
243    /// Raw pipeline swap — bypasses the lifecycle/aggregate rejection check.
244    /// Only safe after the route has been stopped (Restart path).
245    pub(crate) async fn swap_route_pipeline_raw(
246        &self,
247        route_id: &str,
248        pipeline: camel_api::BoxProcessor,
249        lifecycle: Vec<Arc<dyn camel_api::StepLifecycle>>,
250    ) -> Result<(), CamelError> {
251        self.controller
252            .swap_pipeline_raw(route_id, pipeline, lifecycle)
253            .await
254    }
255
256    pub(crate) async fn execute_runtime_command(
257        &self,
258        cmd: camel_api::RuntimeCommand,
259    ) -> Result<camel_api::RuntimeCommandResult, CamelError> {
260        self.runtime.execute(cmd).await
261    }
262
263    pub(crate) async fn runtime_route_status(
264        &self,
265        route_id: &str,
266    ) -> Result<Option<String>, CamelError> {
267        match self
268            .runtime
269            .ask(camel_api::RuntimeQuery::GetRouteStatus {
270                route_id: route_id.to_string(),
271            })
272            .await
273        {
274            Ok(camel_api::RuntimeQueryResult::RouteStatus { status, .. }) => Ok(Some(status)),
275            Ok(_) => Err(CamelError::RouteError(
276                "unexpected runtime query response for route status".to_string(),
277            )),
278            Err(CamelError::RouteError(msg)) if msg.contains("not found") => Ok(None),
279            Err(err) => Err(err),
280        }
281    }
282
283    pub(crate) async fn runtime_route_ids(&self) -> Result<Vec<String>, CamelError> {
284        match self.runtime.ask(camel_api::RuntimeQuery::ListRoutes).await {
285            Ok(camel_api::RuntimeQueryResult::Routes { route_ids }) => Ok(route_ids),
286            Ok(_) => Err(CamelError::RouteError(
287                "unexpected runtime query response for route listing".to_string(),
288            )),
289            Err(err) => Err(err),
290        }
291    }
292
293    pub(crate) async fn route_source_hash(&self, route_id: &str) -> Option<u64> {
294        self.controller.route_source_hash(route_id).await
295    }
296
297    pub(crate) async fn in_flight_count(&self, route_id: &str) -> Result<u64, CamelError> {
298        if !self.controller.route_exists(route_id).await? {
299            return Err(CamelError::RouteError(format!(
300                "Route '{}' not found",
301                route_id
302            )));
303        }
304        Ok(self
305            .controller
306            .in_flight_count(route_id)
307            .await?
308            .unwrap_or(0))
309    }
310
311    /// Check whether the running route has lifecycle-bearing steps.
312    pub(crate) async fn route_has_lifecycle(&self, route_id: &str) -> bool {
313        self.controller
314            .route_has_lifecycle(route_id)
315            .await
316            .unwrap_or(false)
317    }
318
319    pub(crate) fn function_invoker(&self) -> Option<Arc<dyn FunctionInvoker>> {
320        self.function_invoker.clone()
321    }
322
323    #[cfg(test)]
324    pub(crate) async fn force_start_route_for_test(
325        &self,
326        route_id: &str,
327    ) -> Result<(), CamelError> {
328        self.controller.start_route(route_id).await
329    }
330
331    pub async fn controller_route_count_for_test(&self) -> usize {
332        self.controller.route_count().await.unwrap_or(0)
333    }
334}
335
336#[async_trait::async_trait]
337impl crate::hot_reload::ports::ReloadExecutorPort for RuntimeExecutionHandle {
338    async fn add_route_definition(&self, definition: RouteDefinition) -> Result<(), CamelError> {
339        RuntimeExecutionHandle::add_route_definition(self, definition).await
340    }
341
342    async fn compile_route_definition_pipeline(
343        &self,
344        definition: RouteDefinition,
345        generation: u64,
346    ) -> Result<crate::lifecycle::domain::CompiledPipeline, CamelError> {
347        RuntimeExecutionHandle::compile_route_definition_pipeline(self, definition, generation)
348            .await
349    }
350
351    async fn compile_route_definition_dry_pipeline(
352        &self,
353        definition: RouteDefinition,
354    ) -> Result<crate::lifecycle::domain::CompiledPipeline, CamelError> {
355        RuntimeExecutionHandle::compile_route_definition_dry_pipeline(self, definition).await
356    }
357
358    async fn prepare_route_definition_with_generation(
359        &self,
360        definition: RouteDefinition,
361        generation: u64,
362    ) -> Result<crate::lifecycle::domain::route_compilation::PreparedRoute, CamelError> {
363        RuntimeExecutionHandle::prepare_route_definition_with_generation(
364            self, definition, generation,
365        )
366        .await
367    }
368
369    async fn insert_prepared_route(
370        &self,
371        prepared: crate::lifecycle::domain::route_compilation::PreparedRoute,
372    ) -> Result<(), CamelError> {
373        RuntimeExecutionHandle::insert_prepared_route(self, prepared).await
374    }
375
376    async fn discard_prepared_staging(&self, route_id: &str) -> Result<(), CamelError> {
377        RuntimeExecutionHandle::discard_prepared_staging(self, route_id).await
378    }
379
380    async fn remove_route_preserving_functions(&self, route_id: String) -> Result<(), CamelError> {
381        RuntimeExecutionHandle::remove_route_preserving_functions(self, route_id).await
382    }
383
384    async fn register_route_aggregate(&self, route_id: String) -> Result<(), CamelError> {
385        RuntimeExecutionHandle::register_route_aggregate(self, route_id).await
386    }
387
388    async fn swap_route_pipeline(
389        &self,
390        route_id: &str,
391        pipeline: camel_api::BoxProcessor,
392    ) -> Result<(), CamelError> {
393        RuntimeExecutionHandle::swap_route_pipeline(self, route_id, pipeline).await
394    }
395
396    async fn stop_route_reload(&self, route_id: &str) -> Result<(), CamelError> {
397        RuntimeExecutionHandle::stop_route_reload(self, route_id).await
398    }
399
400    async fn start_route_reload(&self, route_id: &str) -> Result<(), CamelError> {
401        RuntimeExecutionHandle::start_route_reload(self, route_id).await
402    }
403
404    async fn swap_route_pipeline_raw(
405        &self,
406        route_id: &str,
407        pipeline: camel_api::BoxProcessor,
408        lifecycle: Vec<std::sync::Arc<dyn camel_api::StepLifecycle>>,
409    ) -> Result<(), CamelError> {
410        RuntimeExecutionHandle::swap_route_pipeline_raw(self, route_id, pipeline, lifecycle).await
411    }
412
413    async fn execute_runtime_command(
414        &self,
415        cmd: camel_api::RuntimeCommand,
416    ) -> Result<camel_api::RuntimeCommandResult, CamelError> {
417        RuntimeExecutionHandle::execute_runtime_command(self, cmd).await
418    }
419
420    async fn runtime_route_status(&self, route_id: &str) -> Result<Option<String>, CamelError> {
421        RuntimeExecutionHandle::runtime_route_status(self, route_id).await
422    }
423
424    async fn in_flight_count(&self, route_id: &str) -> Result<u64, CamelError> {
425        RuntimeExecutionHandle::in_flight_count(self, route_id).await
426    }
427
428    async fn route_has_lifecycle(&self, route_id: &str) -> bool {
429        RuntimeExecutionHandle::route_has_lifecycle(self, route_id).await
430    }
431
432    #[cfg(test)]
433    fn take_test_lifecycle_inject(
434        &self,
435    ) -> Option<Vec<std::sync::Arc<dyn camel_api::StepLifecycle>>> {
436        self.test_lifecycle_inject.lock().unwrap().take()
437    }
438}
439
440impl CamelContext {
441    pub fn builder() -> CamelContextBuilder {
442        CamelContextBuilder::new()
443    }
444
445    /// Set a global error handler applied to all routes without a per-route handler.
446    pub async fn set_error_handler(&mut self, config: ErrorHandlerConfig) {
447        let _ = self.route_controller.set_error_handler(config).await;
448    }
449
450    /// Install per-bind public-exposure acknowledgements (ADR-0061).
451    /// Built by the CLI from `CamelConfig.binds`; the per-bind gate fails
452    /// closed on non-loopback binds until acknowledged.
453    pub async fn set_bind_exposure_acks(
454        &mut self,
455        acks: crate::lifecycle::adapters::route_controller_trait::BindExposureAcks,
456    ) {
457        let _ = self.route_controller.set_bind_exposure_acks(acks).await;
458    }
459
460    /// Enable or disable tracing globally.
461    pub async fn set_tracing(&mut self, enabled: bool) {
462        let _ = self
463            .route_controller
464            .set_tracer_config(TracerConfig {
465                enabled,
466                ..Default::default()
467            })
468            .await;
469    }
470
471    /// Configure tracing with full config.
472    pub async fn set_tracer_config(&mut self, config: TracerConfig) {
473        // Inject metrics collector if not already set
474        let config = if config.metrics_collector.is_none() {
475            TracerConfig {
476                metrics_collector: Some(Arc::clone(&self.metrics)),
477                ..config
478            }
479        } else {
480            config
481        };
482
483        let _ = self.route_controller.set_tracer_config(config).await;
484    }
485
486    /// Builder-style: enable tracing with default config.
487    pub async fn with_tracing(mut self) -> Self {
488        self.set_tracing(true).await;
489        self
490    }
491
492    /// Builder-style: configure tracing with custom config.
493    /// Note: tracing subscriber initialization (stdout/file output) is handled
494    /// separately via init_tracing_subscriber (called in camel-config bridge).
495    pub async fn with_tracer_config(mut self, config: TracerConfig) -> Self {
496        self.set_tracer_config(config).await;
497        self
498    }
499
500    /// Register a lifecycle service (Apache Camel: addService pattern)
501    ///
502    /// For services exposing `as_function_invoker()`, the invoker is propagated
503    /// to the route controller so that subsequent route definitions with function
504    /// steps work correctly.
505    ///
506    /// Prefer [`CamelContextBuilder::with_lifecycle`] when possible, which wires
507    /// the invoker at build time before any routes are added.
508    pub fn with_lifecycle<L: Lifecycle + 'static>(mut self, service: L) -> Self {
509        if let Some(collector) = service.as_metrics_collector() {
510            self.metrics = collector;
511        }
512        if let Some(invoker) = service.as_function_invoker() {
513            self.function_invoker = Some(invoker.clone());
514            if let Err(e) = self.route_controller.try_set_function_invoker(invoker) {
515                tracing::debug!("Failed to propagate function invoker to route controller: {e}");
516            }
517        }
518
519        self.services.push(Box::new(service));
520        self
521    }
522
523    /// Register a component with this context.
524    ///
525    /// Delegates to [`register_component_dyn`](Self::register_component_dyn)
526    /// so metadata harvesting happens regardless of which entry point
527    /// is used.
528    pub fn register_component<C: Component + 'static>(&mut self, component: C) {
529        self.register_component_dyn(Arc::new(component));
530    }
531
532    /// Install route send-point interception rules (pre-first-use only).
533    ///
534    /// Returns `CamelError::Config` once frozen: after the first route is
535    /// registered or the context is started, compiled pipelines have
536    /// captured the rule set and it cannot change. Builder-time rules via
537    /// [`CamelContextBuilder::with_intercept_rules`] bypass this gate.
538    pub async fn set_intercept_rules(&self, rules: InterceptRules) -> Result<(), CamelError> {
539        self.route_controller.set_intercept_rules(rules).await
540    }
541
542    /// Register a startup `ConfigCheck` to be evaluated at the head of
543    /// [`start()`](Self::start). Established by ADR-0033.
544    ///
545    /// Checks are drained and executed synchronously before any route consumer
546    /// is started. If any check returns `Err`, `start()` fails closed with
547    /// `CamelError::Config(_)` and no route is started. The check list is
548    /// consumed (moved) during `start()` so this method may be called multiple
549    /// times to register an arbitrary number of checks.
550    pub fn add_startup_check(&mut self, check: Box<dyn ConfigCheck>) {
551        self.startup_checks.push(check);
552    }
553
554    /// Register a language with this context, keyed by name.
555    ///
556    /// Returns `Err(LanguageRegistryError::AlreadyRegistered)` if a language
557    /// with the same name is already registered. Use
558    /// [`resolve_language`](Self::resolve_language) to check before
559    /// registering, or choose a distinct name.
560    pub fn register_language(
561        &mut self,
562        name: impl Into<String>,
563        lang: Box<dyn Language>,
564    ) -> Result<(), LanguageRegistryError> {
565        let name = name.into();
566        let mut languages = self
567            .languages
568            .lock()
569            .expect("mutex poisoned: another thread panicked while holding this lock"); // allow-unwrap
570        if languages.contains_key(&name) {
571            return Err(LanguageRegistryError::AlreadyRegistered { name });
572        }
573        languages.insert(name, Arc::from(lang));
574        Ok(())
575    }
576
577    /// Resolve a language by name. Returns `None` if not registered.
578    pub fn resolve_language(&self, name: &str) -> Option<Arc<dyn Language>> {
579        let languages = self
580            .languages
581            .lock()
582            .expect("mutex poisoned: another thread panicked while holding this lock"); // allow-unwrap
583        languages.get(name).cloned()
584    }
585
586    /// Add a route definition to this context.
587    ///
588    /// The route must have an ID. Steps are resolved immediately using registered components.
589    pub async fn add_route_definition(
590        &self,
591        definition: RouteDefinition,
592    ) -> Result<(), CamelError> {
593        use crate::lifecycle::application::ports::RouteRegistrationPort;
594        debug!(
595            from = definition.from_uri(),
596            route_id = %definition.route_id(),
597            "Adding route definition"
598        );
599        self.runtime
600            .register_route(definition)
601            .await
602            .map_err(Into::into)
603    }
604
605    /// Access the component registry.
606    pub fn registry(&self) -> std::sync::MutexGuard<'_, Registry> {
607        self.registry
608            .lock()
609            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
610    }
611
612    /// Access the shared component registry Arc.
613    pub fn registry_arc(&self) -> Arc<std::sync::Mutex<Registry>> {
614        Arc::clone(&self.registry)
615    }
616
617    /// Get runtime execution handle for file-watcher integrations.
618    pub fn runtime_execution_handle(&self) -> RuntimeExecutionHandle {
619        RuntimeExecutionHandle {
620            controller: self.route_controller.clone(),
621            runtime: Arc::clone(&self.runtime),
622            function_invoker: self.function_invoker.clone(),
623            #[cfg(test)]
624            test_lifecycle_inject: Arc::new(std::sync::Mutex::new(None)),
625        }
626    }
627
628    /// Get the metrics collector.
629    pub fn metrics(&self) -> Arc<dyn MetricsCollector> {
630        Arc::clone(&self.metrics)
631    }
632
633    /// Get the platform service.
634    pub fn platform_service(&self) -> Arc<dyn PlatformService> {
635        Arc::clone(&self.platform_service)
636    }
637
638    /// Get the readiness gate port.
639    pub fn readiness_gate(&self) -> Arc<dyn ReadinessGate> {
640        self.platform_service.readiness_gate()
641    }
642
643    /// Get the platform identity.
644    pub fn platform_identity(&self) -> PlatformIdentity {
645        self.platform_service.identity()
646    }
647
648    /// Get the leadership service port.
649    pub fn leadership(&self) -> Arc<dyn camel_api::LeadershipService> {
650        self.platform_service.leadership()
651    }
652
653    /// Get runtime command/query bus handle.
654    pub fn runtime(&self) -> Arc<dyn camel_api::RuntimeHandle> {
655        self.runtime.clone()
656    }
657
658    /// Build a producer context wired to this runtime.
659    pub fn producer_context(&self) -> camel_api::ProducerContext {
660        camel_api::ProducerContext::new().with_runtime(self.runtime())
661    }
662
663    /// Query route status via runtime read-model.
664    pub async fn runtime_route_status(&self, route_id: &str) -> Result<Option<String>, CamelError> {
665        match self
666            .runtime()
667            .ask(camel_api::RuntimeQuery::GetRouteStatus {
668                route_id: route_id.to_string(),
669            })
670            .await
671        {
672            Ok(camel_api::RuntimeQueryResult::RouteStatus { status, .. }) => Ok(Some(status)),
673            Ok(_) => Err(CamelError::RouteError(
674                "unexpected runtime query response for route status".to_string(),
675            )),
676            Err(CamelError::RouteError(msg)) if msg.contains("not found") => Ok(None),
677            Err(err) => Err(err),
678        }
679    }
680
681    /// Start all routes. Each route's consumer will begin producing exchanges.
682    ///
683    /// Only routes with `auto_startup == true` will be started, in order of their
684    /// `startup_order` (lower values start first).
685    ///
686    /// Algorithm lives in `lifecycle::application::context_lifecycle::start_context`
687    /// (Tier C C2). Public signature is unchanged.
688    pub async fn start(&mut self) -> Result<(), CamelError> {
689        crate::lifecycle::application::context_lifecycle::start_context(
690            &mut self.services,
691            &mut self.startup_checks,
692            &self.runtime,
693            &self.route_controller,
694            &mut self.cancel_token,
695        )
696        .await?;
697        // Trip the intercept-rules freeze so it applies even with zero
698        // routes. A failed `start_context` above returns early — a failed
699        // start does not freeze.
700        self.route_controller.mark_started().await
701    }
702
703    /// Graceful shutdown with default 30-second timeout.
704    pub async fn stop(&mut self) -> Result<(), CamelError> {
705        self.stop_timeout(self.shutdown_timeout).await
706    }
707
708    /// Graceful shutdown with custom timeout.
709    ///
710    /// Note: The timeout parameter is currently not propagated to the
711    /// RouteController's per-route shutdown timeout. The RouteController
712    /// uses a hardcoded 5-second default (`DEFAULT_SHUTDOWN_TIMEOUT`).
713    /// Full propagation is planned for a future version.
714    ///
715    /// Algorithm lives in `lifecycle::application::context_lifecycle::stop_context`
716    /// (Tier C C2). Public signature is unchanged.
717    pub async fn stop_timeout(&mut self, _timeout: std::time::Duration) -> Result<(), CamelError> {
718        crate::lifecycle::application::context_lifecycle::stop_context(
719            &self.cancel_token,
720            &mut self.supervision_join,
721            &self.runtime,
722            &self.route_controller,
723            &mut self.services,
724        )
725        .await
726    }
727
728    /// Get the graceful shutdown timeout used by [`stop()`](Self::stop).
729    pub fn shutdown_timeout(&self) -> std::time::Duration {
730        self.shutdown_timeout
731    }
732
733    /// Set the graceful shutdown timeout used by [`stop()`](Self::stop).
734    pub fn set_shutdown_timeout(&mut self, timeout: std::time::Duration) {
735        self.shutdown_timeout = timeout;
736    }
737
738    /// Test-only: take the actor join handle out of the context.
739    /// Used to verify the actor exits gracefully after stop().
740    #[cfg(test)]
741    pub(crate) fn take_actor_join(&mut self) -> Option<tokio::task::JoinHandle<()>> {
742        self.actor_join.take()
743    }
744
745    /// Immediate abort — kills all tasks without draining.
746    ///
747    /// Algorithm lives in `lifecycle::application::context_lifecycle::abort_context`
748    /// (Tier C C2). Public signature is unchanged. The use-case routes
749    /// through `RouteOrderingPort` + `RouteDestructiveTeardownPort`; the
750    /// same underlying `RouteControllerHandle` is passed twice as both
751    /// trait objects.
752    pub async fn abort(&mut self) {
753        crate::lifecycle::application::context_lifecycle::abort_context(
754            &self.cancel_token,
755            &mut self.supervision_join,
756            &self.runtime,
757            &self.route_controller as &dyn crate::lifecycle::application::ports::RouteOrderingPort,
758            &self.route_controller
759                as &dyn crate::lifecycle::application::ports::RouteDestructiveTeardownPort,
760            &mut self.services,
761            self.health_registry.cancel_token(),
762            &mut self.actor_join,
763        )
764        .await
765    }
766
767    /// Check health status of all registered services and lifecycle services.
768    pub async fn health_check(&self) -> HealthReport {
769        use camel_api::HealthSource;
770        self.health_report().await
771    }
772
773    pub fn health_registry(&self) -> Arc<HealthCheckRegistry> {
774        Arc::clone(&self.health_registry)
775    }
776
777    /// Store a component config. Overwrites any previously stored config of the same type.
778    pub fn set_component_config<T: 'static + Send + Sync>(&mut self, config: T) {
779        self.component_configs
780            .insert(TypeId::of::<T>(), Box::new(config));
781    }
782
783    /// Retrieve a stored component config by type. Returns None if not stored.
784    pub fn get_component_config<T: 'static + Send + Sync>(&self) -> Option<&T> {
785        self.component_configs
786            .get(&TypeId::of::<T>())
787            .and_then(|b| b.downcast_ref::<T>())
788    }
789
790    // --- Component Metadata ---
791
792    /// Get a component's harvested metadata by URI scheme.
793    pub fn component_metadata(&self, scheme: &str) -> Option<ComponentMetadata> {
794        self.registry.lock().ok()?.get_metadata(scheme)
795    }
796
797    /// Get metadata for every registered component.
798    pub fn all_component_metadata(&self) -> Vec<ComponentMetadata> {
799        self.registry
800            .lock()
801            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
802            .all_metadata()
803    }
804
805    /// Get a metadata catalog handle implementing
806    /// [`ComponentMetadataCatalog`](camel_api::component_metadata::ComponentMetadataCatalog).
807    ///
808    /// The returned handle shares the same `Arc<Mutex<Registry>>` as the
809    /// context, so registrations made through one are visible through the
810    /// other.
811    pub fn metadata_catalog(
812        &self,
813    ) -> crate::component_metadata_catalog::RuntimeComponentMetadataCatalog {
814        crate::component_metadata_catalog::RuntimeComponentMetadataCatalog::new(Arc::clone(
815            &self.registry,
816        ))
817    }
818
819    // --- Route Template Registry (data-only) ---
820
821    /// Register a route template specification.
822    ///
823    /// Returns `Err(CamelError)` if a template with the same ID is already registered.
824    pub fn add_route_template(&self, spec: RouteTemplateSpec) -> Result<(), CamelError> {
825        self.template_registry.register(spec)
826    }
827
828    /// Retrieve a route template specification by its ID.
829    pub fn get_route_template(&self, id: &str) -> Option<RouteTemplateSpec> {
830        self.template_registry.get(id)
831    }
832
833    /// Return all registered template IDs.
834    pub fn template_ids(&self) -> Vec<String> {
835        self.template_registry.template_ids()
836    }
837
838    /// Record a newly instantiated template instance.
839    pub fn record_template_instance(&self, record: TemplateInstanceRecord) {
840        self.template_registry.record_instance(record)
841    }
842
843    /// Return all instance records for a given template ID.
844    pub fn template_instances(&self, template_id: &str) -> Vec<TemplateInstanceRecord> {
845        self.template_registry.instances(template_id)
846    }
847
848    // --- Idempotent Repository Registry ---
849
850    /// Register an idempotent repository.
851    ///
852    /// Returns `Err(RegistryError::AlreadyRegistered)` if a repository with
853    /// the same name is already registered.
854    pub fn register_idempotent_repository(
855        &mut self,
856        name: impl Into<String>,
857        repo: Arc<dyn camel_api::IdempotentRepository>,
858    ) -> Result<(), RegistryError> {
859        self.idempotent_repositories.register(name, repo)
860    }
861
862    /// Retrieve an idempotent repository by name.
863    pub fn idempotent_repository(
864        &self,
865        name: &str,
866    ) -> Option<Arc<dyn camel_api::IdempotentRepository>> {
867        self.idempotent_repositories.get(name)
868    }
869
870    // --- Claim Check Repository Registry ---
871
872    /// Register a claim check repository.
873    ///
874    /// Returns `Err(RegistryError::AlreadyRegistered)` if a repository with
875    /// the same name is already registered.
876    pub fn register_claim_check_repository(
877        &mut self,
878        name: impl Into<String>,
879        repo: Arc<dyn camel_api::ClaimCheckRepository>,
880    ) -> Result<(), RegistryError> {
881        self.claim_check_repositories.register(name, repo)
882    }
883
884    /// Retrieve a claim check repository by name.
885    pub fn claim_check_repository(
886        &self,
887        name: &str,
888    ) -> Option<Arc<dyn camel_api::ClaimCheckRepository>> {
889        self.claim_check_repositories.get(name)
890    }
891
892    // --- Cache Repository Registry ---
893
894    /// Register a cache repository.
895    ///
896    /// Returns `Err(RegistryError::AlreadyRegistered)` if a repository with
897    /// the same name is already registered.
898    pub fn register_cache_repository(
899        &mut self,
900        name: impl Into<String>,
901        repo: Arc<dyn camel_api::CacheRepository>,
902    ) -> Result<(), RegistryError> {
903        self.cache_repositories.register(name, repo)
904    }
905
906    /// Replace an existing cache repository, returning the evicted value.
907    ///
908    /// Returns `None` if no repository was registered under `name`.
909    pub fn replace_cache_repository(
910        &mut self,
911        name: impl Into<String>,
912        repo: Arc<dyn camel_api::CacheRepository>,
913    ) -> Option<Arc<dyn camel_api::CacheRepository>> {
914        self.cache_repositories.register_or_replace(name, repo)
915    }
916
917    /// Retrieve a cache repository by name.
918    pub fn cache_repository(&self, name: &str) -> Option<Arc<dyn camel_api::CacheRepository>> {
919        self.cache_repositories.get(name)
920    }
921
922    /// Access the shutdown cancellation token.
923    ///
924    /// Repositories that need to bind background sweep tasks to context
925    /// shutdown (e.g. `RedbCacheRepository`) can `child_token()` from this.
926    pub fn shutdown_token(&self) -> CancellationToken {
927        self.cancel_token.clone()
928    }
929}
930
931impl ComponentRegistrar for CamelContext {
932    fn register_component_dyn(&mut self, component: Arc<dyn Component>) {
933        let scheme = component.scheme().to_string();
934        self.registry
935            .lock()
936            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
937            .register(component);
938        trace!(scheme, "Registered component");
939    }
940}
941
942impl ComponentContext for CamelContext {
943    fn resolve_component(&self, scheme: &str) -> Option<Arc<dyn Component>> {
944        self.registry.lock().ok()?.get(scheme)
945    }
946
947    fn resolve_language(&self, name: &str) -> Option<Arc<dyn Language>> {
948        self.languages.lock().ok()?.get(name).cloned()
949    }
950
951    fn metrics(&self) -> Arc<dyn MetricsCollector> {
952        Arc::clone(&self.metrics)
953    }
954
955    fn health(&self) -> Arc<dyn camel_component_api::HealthCheckRegistry> {
956        // The concrete HealthCheckRegistry struct implements the trait via
957        // the impl added in health_registry.rs.
958        Arc::clone(&self.health_registry) as Arc<dyn camel_component_api::HealthCheckRegistry>
959    }
960
961    fn platform_service(&self) -> Arc<dyn PlatformService> {
962        Arc::clone(&self.platform_service)
963    }
964
965    fn register_route_health_check(
966        &self,
967        route_id: &str,
968        check: Arc<dyn camel_api::AsyncHealthCheck>,
969    ) {
970        self.health_registry.register_for_route(route_id, check);
971    }
972
973    fn unregister_route_health_check(&self, route_id: &str) {
974        self.health_registry.unregister_for_route(route_id);
975    }
976}
977
978#[async_trait::async_trait]
979impl camel_api::HealthSource for CamelContext {
980    async fn liveness(&self) -> camel_api::HealthStatus {
981        let has_failed = self
982            .services
983            .iter()
984            .any(|s| s.status() == camel_api::ServiceStatus::Failed);
985        if has_failed {
986            camel_api::HealthStatus::Unhealthy
987        } else {
988            camel_api::HealthStatus::Healthy
989        }
990    }
991
992    async fn readiness(&self) -> camel_api::HealthStatus {
993        let has_failed = self
994            .services
995            .iter()
996            .any(|s| s.status() == camel_api::ServiceStatus::Failed);
997        if has_failed {
998            return camel_api::HealthStatus::Unhealthy;
999        }
1000        let has_stopped = self
1001            .services
1002            .iter()
1003            .any(|s| s.status() == camel_api::ServiceStatus::Stopped);
1004        if has_stopped {
1005            return camel_api::HealthStatus::Degraded;
1006        }
1007        self.health_registry.check_all().await.status
1008    }
1009
1010    async fn health_report(&self) -> camel_api::HealthReport {
1011        let mut report = self.health_registry.check_all().await;
1012        let mut worst = report.status;
1013        for service in &self.services {
1014            let svc_status = service.status();
1015            let health = match svc_status {
1016                camel_api::ServiceStatus::Started => camel_api::HealthStatus::Healthy,
1017                camel_api::ServiceStatus::Stopped => camel_api::HealthStatus::Degraded,
1018                camel_api::ServiceStatus::Failed => camel_api::HealthStatus::Unhealthy,
1019                // Forward-safe fail-closed: an unknown future ServiceStatus keeps
1020                // the pod NotReady (no traffic) rather than guessing Degraded.
1021                _ => camel_api::HealthStatus::Unhealthy,
1022            };
1023            if matches!(worst, camel_api::HealthStatus::Healthy)
1024                && matches!(
1025                    health,
1026                    camel_api::HealthStatus::Degraded | camel_api::HealthStatus::Unhealthy
1027                )
1028            {
1029                worst = health;
1030            }
1031            if matches!(worst, camel_api::HealthStatus::Degraded)
1032                && matches!(health, camel_api::HealthStatus::Unhealthy)
1033            {
1034                worst = health;
1035            }
1036            report.services.push(camel_api::ServiceHealth {
1037                name: service.name().to_string(),
1038                status: svc_status,
1039                message: None,
1040            });
1041        }
1042        report.status = worst;
1043        report
1044    }
1045
1046    async fn startup(&self) -> camel_api::HealthStatus {
1047        camel_api::HealthStatus::Healthy
1048    }
1049}
1050
1051#[cfg(test)]
1052#[path = "context_tests.rs"]
1053mod context_tests;