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