Skip to main content

camel_core/
context_builder.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use tokio_util::sync::CancellationToken;
4
5use camel_api::{
6    CamelError, FunctionInvoker, InFlightGauge, Lifecycle, MetricsCollector, MetricsHandle,
7    NoopPlatformService, PlatformService, SupervisionConfig,
8};
9use camel_language_api::Language;
10
11use super::context::{CamelContext, FromParts};
12use crate::cache::memory::MemoryCacheRepository;
13use crate::claim_check::memory_repository::MemoryClaimCheckRepository;
14use crate::health_registry::HealthCheckRegistry;
15use crate::idempotent::memory_repository::MemoryIdempotentRepository;
16use crate::intercept::InterceptRules;
17use crate::lifecycle::adapters::RuntimeExecutionAdapter;
18use crate::lifecycle::adapters::controller_actor::{
19    RouteControllerHandle, spawn_controller_actor, spawn_supervision_task,
20};
21use crate::lifecycle::adapters::route_controller::{
22    DefaultRouteController, SharedLanguageRegistry,
23};
24use crate::lifecycle::application::ports::RuntimeExecutionPort;
25use crate::lifecycle::application::runtime_bus::RuntimeBus;
26use crate::registry::{CacheRegistry, ClaimCheckRegistry, IdempotentRegistry};
27use crate::shared::components::domain::Registry;
28use crate::startup_validation::ConfigCheck;
29use crate::template::TemplateRegistry;
30
31type ExecutionFactory =
32    Arc<dyn Fn(RouteControllerHandle) -> Arc<dyn RuntimeExecutionPort> + Send + Sync>;
33
34pub struct CamelContextBuilder {
35    registry: Option<Arc<std::sync::Mutex<Registry>>>,
36    languages: Option<SharedLanguageRegistry>,
37    metrics: Option<Arc<dyn MetricsCollector>>,
38    // Platform ports
39    platform_service: Option<Arc<dyn PlatformService>>,
40    supervision_config: Option<SupervisionConfig>,
41    runtime_store: Option<crate::lifecycle::adapters::InMemoryRuntimeStore>,
42    shutdown_timeout: std::time::Duration,
43    beans: Option<Arc<std::sync::Mutex<camel_bean::BeanRegistry>>>,
44    function_invoker: Option<Arc<dyn FunctionInvoker>>,
45    lifecycle_services: Vec<Box<dyn Lifecycle>>,
46    execution_factory: Option<ExecutionFactory>,
47    health_registry: Option<Arc<HealthCheckRegistry>>,
48    template_registry: Option<Arc<TemplateRegistry>>,
49    intercept_rules: Option<InterceptRules>,
50}
51
52/// Push `camel_uptime_seconds` onto the collector every 60s.
53///
54/// The Prometheus endpoint is pull-based — no periodic path in
55/// camel-prometheus reaches the collector — so the runtime pushes the
56/// gauge itself. The task exits when `shutdown` (the context's token)
57/// fires; uptime is anchored at `started` (context build time), so the
58/// first scrape after a restart reads near zero.
59fn spawn_uptime_refresh(
60    metrics: Arc<dyn MetricsCollector>,
61    shutdown: CancellationToken,
62    started: std::time::Instant,
63) {
64    tokio::spawn(async move {
65        // The interval's first tick fires immediately; uptime was already
66        // recorded once at build, so consume it and refresh every 60s after.
67        let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
68        interval.tick().await;
69        loop {
70            tokio::select! {
71                _ = shutdown.cancelled() => break,
72                _ = interval.tick() => {
73                    metrics.record_uptime(started.elapsed().as_secs_f64());
74                }
75            }
76        }
77    });
78}
79
80impl CamelContextBuilder {
81    pub fn new() -> Self {
82        Self {
83            registry: None,
84            languages: None,
85            metrics: None,
86            platform_service: None,
87            supervision_config: None,
88            runtime_store: None,
89            shutdown_timeout: std::time::Duration::from_secs(5),
90            beans: None,
91            function_invoker: None,
92            lifecycle_services: Vec::new(),
93            execution_factory: None,
94            health_registry: None,
95            template_registry: None,
96            intercept_rules: None,
97        }
98    }
99
100    pub fn registry(mut self, registry: Arc<std::sync::Mutex<Registry>>) -> Self {
101        self.registry = Some(registry);
102        self
103    }
104
105    pub fn languages(mut self, languages: SharedLanguageRegistry) -> Self {
106        self.languages = Some(languages);
107        self
108    }
109
110    pub fn with_execution_factory(
111        mut self,
112        factory: impl Fn(RouteControllerHandle) -> Arc<dyn RuntimeExecutionPort> + Send + Sync + 'static,
113    ) -> Self {
114        self.execution_factory = Some(Arc::new(factory));
115        self
116    }
117
118    pub fn metrics(mut self, metrics: Arc<dyn MetricsCollector>) -> Self {
119        self.metrics = Some(metrics);
120        self
121    }
122
123    /// Set a custom platform service.
124    pub fn platform_service(mut self, platform_service: Arc<dyn PlatformService>) -> Self {
125        self.platform_service = Some(platform_service);
126        self
127    }
128
129    pub fn supervision(mut self, config: SupervisionConfig) -> Self {
130        self.supervision_config = Some(config);
131        self
132    }
133
134    pub fn runtime_store(
135        mut self,
136        store: crate::lifecycle::adapters::InMemoryRuntimeStore,
137    ) -> Self {
138        self.runtime_store = Some(store);
139        self
140    }
141
142    pub fn shutdown_timeout(mut self, timeout: std::time::Duration) -> Self {
143        self.shutdown_timeout = timeout;
144        self
145    }
146
147    pub fn health_registry(mut self, registry: Arc<HealthCheckRegistry>) -> Self {
148        self.health_registry = Some(registry);
149        self
150    }
151
152    /// Inject a shared `BeanRegistry` for bean resolution across routes.
153    pub fn beans(mut self, beans: Arc<std::sync::Mutex<camel_bean::BeanRegistry>>) -> Self {
154        self.beans = Some(beans);
155        self
156    }
157
158    /// Register a lifecycle service (e.g., FunctionRuntimeService) at builder time.
159    ///
160    /// This is the recommended path for services that need to be wired into the
161    /// route controller before any routes are added. The function invoker (if any)
162    /// is extracted and passed to the `DefaultRouteController` during `build()`.
163    pub fn with_lifecycle<L: Lifecycle + 'static>(mut self, service: L) -> Self {
164        if let Some(collector) = service.as_metrics_collector() {
165            self.metrics = Some(collector);
166        }
167        if let Some(invoker) = service.as_function_invoker() {
168            self.function_invoker = Some(invoker);
169        }
170        self.lifecycle_services.push(Box::new(service));
171        self
172    }
173
174    /// Set a custom `TemplateRegistry` for route template storage.
175    ///
176    /// If not provided, a default empty registry is created during `build()`.
177    pub fn template_registry(mut self, registry: Arc<TemplateRegistry>) -> Self {
178        self.template_registry = Some(registry);
179        self
180    }
181
182    /// Set route send-point interception rules at build time.
183    ///
184    /// The rules are installed on a fresh controller, where the
185    /// first-use freeze cannot have tripped yet.
186    pub fn with_intercept_rules(mut self, rules: InterceptRules) -> Self {
187        self.intercept_rules = Some(rules);
188        self
189    }
190
191    fn built_in_languages() -> SharedLanguageRegistry {
192        crate::language_registry::from_config(&camel_language_api::LanguagesConfig::default())
193    }
194
195    fn build_runtime(
196        controller: RouteControllerHandle,
197        store: crate::lifecycle::adapters::InMemoryRuntimeStore,
198        execution_factory: Option<ExecutionFactory>,
199        health_registry: Arc<HealthCheckRegistry>,
200        metrics: Arc<dyn MetricsCollector>,
201    ) -> Arc<RuntimeBus> {
202        let execution: Arc<dyn RuntimeExecutionPort> = if let Some(factory) = execution_factory {
203            factory(controller.clone())
204        } else {
205            Arc::new(RuntimeExecutionAdapter::new(controller))
206        };
207        // The store is the single choke point every RouteStatusProjection
208        // write flows through (both the UoW persist path and the
209        // projection-store upsert path), so seeding it with the SAME shared
210        // late-bound handle keeps `camel_route_state` emission on every
211        // lifecycle transition. No new collector instances.
212        let store = store.with_metrics(Arc::clone(&metrics));
213        Arc::new(
214            RuntimeBus::new(
215                Arc::new(store.clone()),
216                Arc::new(store.clone()),
217                Arc::new(store.clone()),
218                Arc::new(store.clone()),
219            )
220            .with_uow(Arc::new(store))
221            .with_execution(execution)
222            .with_health_registry(health_registry)
223            .with_metrics(metrics),
224        )
225    }
226
227    pub async fn build(self) -> Result<CamelContext, CamelError> {
228        let registry = self
229            .registry
230            .unwrap_or_else(|| Arc::new(std::sync::Mutex::new(Registry::new())));
231        let languages = self.languages.unwrap_or_else(Self::built_in_languages);
232        let simple_with_resolver: Arc<dyn Language> = Arc::new(
233            camel_language_simple::SimpleLanguage::with_resolver(Arc::new({
234                let languages = Arc::clone(&languages);
235                move |name| {
236                    languages
237                        .lock()
238                        .ok()
239                        .and_then(|registry| registry.get(name).cloned())
240                }
241            })),
242        );
243        languages
244            .lock()
245            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
246            .insert("simple".to_string(), simple_with_resolver);
247        // The shared late-bound metrics cell (rc-hrm1.3): created ONCE here,
248        // seeded NoOp, and the SAME Arc seeds the route controller's
249        // `tracer_metrics`, the RuntimeBus collector, and the CamelContext
250        // slot. Collectors pre-registered via the builder (`.metrics()` or
251        // `with_lifecycle`) compose into it; later registrations flow through
252        // `CamelContext::with_lifecycle` without re-snapshotting.
253        let metrics_handle = Arc::new(MetricsHandle::new());
254        if let Some(collector) = self.metrics {
255            metrics_handle.register(collector);
256        }
257        // Build + uptime info (dashboard-observability T3.2): emitted on the
258        // shared handle so every registered collector observes them. There is
259        // no vergen build script yet, so git_sha falls back to "unknown"
260        // (accepted trade-off; an optional build.rs is a follow-up, not part
261        // of this change).
262        metrics_handle.record_build_info(
263            env!("CARGO_PKG_VERSION"),
264            option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"),
265        );
266        let started_at = std::time::Instant::now();
267        let cancel_token = CancellationToken::new();
268        // Slot mirror of `cancel_token` (see
269        // `CamelContext::shutdown_token_slot`): seeded with a clone of the
270        // SAME initial token — clones share cancellation state, so the
271        // mirror matches the initial lineage exactly. `start_context`
272        // rewrites both together on every boot.
273        let shutdown_token_slot = Arc::new(std::sync::Mutex::new(cancel_token.clone()));
274        spawn_uptime_refresh(
275            Arc::clone(&metrics_handle) as Arc<dyn MetricsCollector>,
276            cancel_token.clone(),
277            started_at,
278        );
279        let platform_service = self
280            .platform_service
281            .unwrap_or_else(|| Arc::new(NoopPlatformService::default()));
282        let health_registry = self.health_registry.unwrap_or_else(|| {
283            Arc::new(HealthCheckRegistry::new(std::time::Duration::from_secs(5)))
284        });
285
286        // Default idempotent repository registry with a built-in memory repo.
287        // Built BEFORE the controller so the same Arc can be shared between
288        // CamelContext (user-facing register API) and DefaultRouteController
289        // (compile-time repository-name resolution for the idempotent_consumer step).
290        let idempotent_repositories: crate::registry::SharedIdempotentRegistry = {
291            let reg = Arc::new(IdempotentRegistry::new());
292            let memory = Arc::new(MemoryIdempotentRepository::new("memory"));
293            // If registration fails (e.g. someone already registered "memory"),
294            // it's a programming error — unwrap is safe.
295            reg.register("memory", memory)
296                .expect("built-in memory idempotent repository registration must succeed"); // allow-unwrap
297            reg
298        };
299
300        // Default claim check repository registry with a built-in memory repo.
301        let claim_check_repositories: crate::registry::SharedClaimCheckRegistry = {
302            let reg = Arc::new(ClaimCheckRegistry::new());
303            let memory = Arc::new(MemoryClaimCheckRepository::new("memory"));
304            reg.register("memory", memory)
305                .expect("built-in memory claim check repository registration must succeed"); // allow-unwrap
306            reg
307        };
308
309        // Default cache repository registry with a built-in memory repo.
310        let cache_repositories: crate::registry::SharedCacheRegistry = {
311            let reg = Arc::new(CacheRegistry::new());
312            let memory = Arc::new(MemoryCacheRepository::new("memory", 10_000));
313            reg.register("memory", memory)
314                .expect("built-in memory cache repository registration must succeed"); // allow-unwrap
315            reg
316        };
317
318        // Context-global accepted-not-completed gauge (drainclaim):
319        // created ONCE here. The SAME Arc seeds the route controller
320        // (consumer contexts, producer-creation contexts, the inline
321        // dispatcher) and the CamelContext slot exposed through
322        // `total_in_flight()` — one counter, one linearizable verdict.
323        let in_flight_total = Arc::new(InFlightGauge::new());
324
325        let (controller, actor_join, supervision_join) =
326            if let Some(config) = self.supervision_config {
327                let (crash_tx, crash_rx) = tokio::sync::mpsc::channel(64);
328                let mut controller_impl = if let Some(ref beans) = self.beans {
329                    DefaultRouteController::with_languages_and_beans(
330                        Arc::clone(&registry),
331                        Arc::clone(&languages),
332                        Arc::clone(&platform_service),
333                        Arc::clone(beans),
334                    )
335                } else {
336                    DefaultRouteController::with_languages(
337                        Arc::clone(&registry),
338                        Arc::clone(&languages),
339                        Arc::clone(&platform_service),
340                    )
341                };
342                if let Some(invoker) = self.function_invoker.clone() {
343                    controller_impl = controller_impl.with_function_invoker(invoker);
344                }
345                if let Some(rules) = self.intercept_rules.clone() {
346                    controller_impl = controller_impl.with_intercept_rules(rules);
347                }
348                controller_impl.set_idempotent_repositories(Arc::clone(&idempotent_repositories));
349                controller_impl.set_claim_check_repositories(Arc::clone(&claim_check_repositories));
350                controller_impl.set_cache_repositories(Arc::clone(&cache_repositories));
351                controller_impl.set_health_registry(Arc::clone(&health_registry));
352                controller_impl
353                    .set_tracer_metrics(Arc::clone(&metrics_handle) as Arc<dyn MetricsCollector>);
354                controller_impl.set_in_flight_total(Arc::clone(&in_flight_total));
355                controller_impl.set_crash_notifier(crash_tx);
356                let (controller, actor_join) = spawn_controller_actor(controller_impl);
357                let supervision_join = spawn_supervision_task(
358                    controller.clone(),
359                    config,
360                    Some(Arc::clone(&metrics_handle) as Arc<dyn MetricsCollector>),
361                    crash_rx,
362                );
363                (controller, actor_join, Some(supervision_join))
364            } else {
365                let mut controller_impl = if let Some(ref beans) = self.beans {
366                    DefaultRouteController::with_languages_and_beans(
367                        Arc::clone(&registry),
368                        Arc::clone(&languages),
369                        Arc::clone(&platform_service),
370                        Arc::clone(beans),
371                    )
372                } else {
373                    DefaultRouteController::with_languages(
374                        Arc::clone(&registry),
375                        Arc::clone(&languages),
376                        Arc::clone(&platform_service),
377                    )
378                };
379                if let Some(invoker) = self.function_invoker.clone() {
380                    controller_impl = controller_impl.with_function_invoker(invoker);
381                }
382                if let Some(rules) = self.intercept_rules.clone() {
383                    controller_impl = controller_impl.with_intercept_rules(rules);
384                }
385                controller_impl.set_idempotent_repositories(Arc::clone(&idempotent_repositories));
386                controller_impl.set_claim_check_repositories(Arc::clone(&claim_check_repositories));
387                controller_impl.set_cache_repositories(Arc::clone(&cache_repositories));
388                controller_impl.set_health_registry(Arc::clone(&health_registry));
389                controller_impl
390                    .set_tracer_metrics(Arc::clone(&metrics_handle) as Arc<dyn MetricsCollector>);
391                controller_impl.set_in_flight_total(Arc::clone(&in_flight_total));
392                let (controller, actor_join) = spawn_controller_actor(controller_impl);
393                (controller, actor_join, None)
394            };
395
396        let store = self.runtime_store.unwrap_or_default();
397        let runtime = Self::build_runtime(
398            controller.clone(),
399            store,
400            self.execution_factory,
401            Arc::clone(&health_registry),
402            Arc::clone(&metrics_handle) as Arc<dyn MetricsCollector>,
403        );
404        let runtime_handle: Arc<dyn camel_api::RuntimeHandle> = runtime.clone();
405        controller
406            .try_set_runtime_handle(runtime_handle)
407            .expect("controller actor mailbox should accept initial runtime handle"); // allow-unwrap
408
409        let template_registry = self
410            .template_registry
411            .unwrap_or_else(|| Arc::new(TemplateRegistry::new()));
412
413        Ok(CamelContext::from_parts(FromParts {
414            registry,
415            route_controller: controller,
416            _actor_join: actor_join,
417            supervision_join,
418            runtime,
419            cancel_token,
420            shutdown_token_slot,
421            metrics: metrics_handle,
422            platform_service,
423            languages,
424            shutdown_timeout: self.shutdown_timeout,
425            services: self.lifecycle_services,
426            health_registry,
427            component_configs: HashMap::new(),
428            function_invoker: self.function_invoker,
429            template_registry,
430            idempotent_repositories,
431            claim_check_repositories,
432            cache_repositories,
433            startup_checks: Vec::<Box<dyn ConfigCheck>>::new(),
434            build_version: env!("CARGO_PKG_VERSION"),
435            build_git_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"),
436            build_started_at: started_at,
437            in_flight_total,
438        }))
439    }
440}
441
442impl Default for CamelContextBuilder {
443    fn default() -> Self {
444        Self::new()
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn builder_default_has_sane_timeout() {
454        let builder = CamelContextBuilder::new();
455        assert_eq!(builder.shutdown_timeout, std::time::Duration::from_secs(5));
456    }
457
458    #[tokio::test]
459    async fn builder_registers_default_memory_idempotent_repository() {
460        let ctx = CamelContext::builder()
461            .build()
462            .await
463            .expect("build context");
464        let repo = ctx.idempotent_repository("memory");
465        assert!(
466            repo.is_some(),
467            "default 'memory' idempotent repository should be registered"
468        );
469    }
470
471    #[tokio::test]
472    async fn builder_registers_default_memory_cache_repository() {
473        let ctx = CamelContext::builder()
474            .build()
475            .await
476            .expect("build context");
477        let repo = ctx.cache_repository("memory");
478        assert!(
479            repo.is_some(),
480            "default 'memory' cache repository should be registered"
481        );
482        assert_eq!(repo.unwrap().name(), "memory");
483    }
484}