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