Skip to main content

camel_core/
context_builder.rs

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