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