camel_component_api/component_context.rs
1use std::sync::Arc;
2
3use camel_api::{
4 AsyncHealthCheck, ComponentMetricsLevers, InFlightGauge, MetricsCollector, PlatformService,
5};
6use camel_language_api::Language;
7use tokio_util::sync::CancellationToken;
8
9use crate::Component;
10
11/// Runtime context passed to components during endpoint creation.
12pub trait ComponentContext: Send + Sync {
13 /// Resolve a component by scheme.
14 fn resolve_component(&self, scheme: &str) -> Option<Arc<dyn Component>>;
15
16 /// Resolve a language by name.
17 fn resolve_language(&self, name: &str) -> Option<Arc<dyn Language>>;
18
19 /// Access the active metrics collector.
20 fn metrics(&self) -> Arc<dyn MetricsCollector>;
21
22 /// Context-global gauge of accepted-not-completed exchanges
23 /// (drainclaim). Production contexts return the gauge installed on
24 /// every `ConsumerContext` at consumer start and read by
25 /// `CamelContext::total_in_flight()` for the drain verdict. Default
26 /// `None` keeps test contexts uncounted.
27 fn in_flight_counter(&self) -> Option<Arc<InFlightGauge>> {
28 None
29 }
30
31 /// Snapshot of the `[observability.metrics].components` configuration —
32 /// resolved per component by
33 /// `RuntimeObservability::component_metrics(component)`. The scoped
34 /// identity (not a global bool) lets an override enable one component
35 /// series while the coarse `components` default stays off.
36 ///
37 /// The snapshot gates only the uniform component-operations family;
38 /// error-family emission is never lever-gated. Default: all off
39 /// (opt-in), so unconfigured contexts keep the component series
40 /// suppressed. `CamelContext` overrides this with its
41 /// `MetricsLeversConfig` snapshot.
42 fn component_metric_levers(&self) -> ComponentMetricsLevers {
43 ComponentMetricsLevers::default()
44 }
45
46 /// Access the active health-check registry.
47 ///
48 /// Used by component code paths that need to pin a route Unhealthy
49 /// (category (g) per ADR-0012). Default: NoOp — tests/examples inherit
50 /// the no-op. Concrete runtimes (CamelContext) override to return the
51 /// real registry.
52 fn health(&self) -> Arc<dyn crate::HealthCheckRegistry> {
53 Arc::new(crate::NoOpHealthCheckRegistry)
54 }
55
56 /// Clone of the Runtime-owned shutdown token when this context is
57 /// bound to one. Producer-side and processor-side code (which has no
58 /// `ConsumerContext`) uses it to observe Runtime shutdown.
59 /// `CamelContext` overrides this with its `shutdown_token()`. Default
60 /// `None` keeps unbound contexts (tests, examples) out of the
61 /// shutdown lineage, so callers mint a local root token instead.
62 /// Callers resolve this per call: CamelContext replaces its shutdown
63 /// token on every start, so a token captured once goes stale across
64 /// stop/start.
65 ///
66 /// # Binding-time boundary
67 ///
68 /// Token lineage binds at component REGISTRATION time. Production
69 /// registration sites construct slot-bound contexts
70 /// (`RegistryComponentContext::with_shutdown_slot`, re-written by
71 /// every `CamelContext::start`) and hand them to long-lived components
72 /// (for example `WasmComponent`'s captured `Arc<dyn ComponentContext>`).
73 /// Endpoint-creation-time adapter contexts
74 /// (`ControllerComponentContext`, `MasterDelegateContext`) must NOT
75 /// snapshot tokens; they keep the `None` default so no stale-across
76 /// stop/start lineage can leak, and callers mint a local root instead.
77 fn shutdown_token(&self) -> Option<CancellationToken> {
78 None
79 }
80
81 /// Access the active platform service.
82 fn platform_service(&self) -> Arc<dyn PlatformService>;
83
84 fn register_route_health_check(&self, route_id: &str, check: Arc<dyn AsyncHealthCheck>);
85
86 fn unregister_route_health_check(&self, route_id: &str);
87
88 fn route_id(&self) -> Option<&str> {
89 None
90 }
91
92 fn register_current_route_health_check(&self, check: Arc<dyn AsyncHealthCheck>) {
93 if let Some(id) = self.route_id() {
94 self.register_route_health_check(id, check);
95 }
96 }
97}
98
99/// Default no-op component context for tests/examples.
100pub struct NoOpComponentContext;
101
102impl ComponentContext for NoOpComponentContext {
103 fn resolve_component(&self, _scheme: &str) -> Option<Arc<dyn Component>> {
104 None
105 }
106
107 fn resolve_language(&self, _name: &str) -> Option<Arc<dyn Language>> {
108 None
109 }
110
111 fn metrics(&self) -> Arc<dyn MetricsCollector> {
112 Arc::new(camel_api::NoOpMetrics)
113 }
114
115 fn platform_service(&self) -> Arc<dyn PlatformService> {
116 Arc::new(camel_api::NoopPlatformService::default())
117 }
118
119 fn register_route_health_check(&self, _route_id: &str, _check: Arc<dyn AsyncHealthCheck>) {}
120
121 fn unregister_route_health_check(&self, _route_id: &str) {}
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn component_context_health_default_is_noop() {
130 let ctx = NoOpComponentContext;
131 let h = ctx.health();
132 // Must not panic.
133 h.force_unhealthy_for_route("any", "any", "any");
134 }
135
136 #[test]
137 fn default_component_metric_levers_are_off() {
138 let ctx = NoOpComponentContext;
139 assert!(
140 !ctx.component_metric_levers().enabled_for("http"),
141 "default levers must keep the component series off"
142 );
143 }
144
145 #[test]
146 fn shutdown_token_default_is_none() {
147 let ctx = NoOpComponentContext;
148 assert!(
149 ctx.shutdown_token().is_none(),
150 "unbound contexts must return None so callers mint a local root"
151 );
152 }
153}