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_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
53fn spawn_uptime_refresh(
61 metrics: Arc<dyn MetricsCollector>,
62 shutdown: CancellationToken,
63 started: std::time::Instant,
64) {
65 tokio::spawn(async move {
66 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 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 pub fn beans(mut self, beans: Arc<std::sync::Mutex<camel_bean::BeanRegistry>>) -> Self {
161 self.beans = Some(beans);
162 self
163 }
164
165 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 pub fn template_registry(mut self, registry: Arc<TemplateRegistry>) -> Self {
185 self.template_registry = Some(registry);
186 self
187 }
188
189 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 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") .insert("simple".to_string(), simple_with_resolver);
254 let metrics_handle = Arc::new(MetricsHandle::new());
261 if let Some(collector) = self.metrics {
262 metrics_handle.register(collector);
263 }
264 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 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 let idempotent_repositories: crate::registry::SharedIdempotentRegistry = {
298 let reg = Arc::new(IdempotentRegistry::new());
299 let memory = Arc::new(MemoryIdempotentRepository::new("memory"));
300 reg.register("memory", memory)
303 .expect("built-in memory idempotent repository registration must succeed"); reg
305 };
306
307 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"); reg
314 };
315
316 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"); reg
323 };
324
325 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(®istry),
338 Arc::clone(&languages),
339 Arc::clone(&platform_service),
340 Arc::clone(beans),
341 )
342 } else {
343 DefaultRouteController::with_languages(
344 Arc::clone(®istry),
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(®istry),
375 Arc::clone(&languages),
376 Arc::clone(&platform_service),
377 Arc::clone(beans),
378 )
379 } else {
380 DefaultRouteController::with_languages(
381 Arc::clone(®istry),
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"); 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}