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