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_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
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 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 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 pub fn beans(mut self, beans: Arc<std::sync::Mutex<camel_bean::BeanRegistry>>) -> Self {
155 self.beans = Some(beans);
156 self
157 }
158
159 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 pub fn template_registry(mut self, registry: Arc<TemplateRegistry>) -> Self {
179 self.template_registry = Some(registry);
180 self
181 }
182
183 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 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") .insert("simple".to_string(), simple_with_resolver);
248 let metrics_handle = Arc::new(MetricsHandle::new());
255 if let Some(collector) = self.metrics {
256 metrics_handle.register(collector);
257 }
258 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 let idempotent_repositories: crate::registry::SharedIdempotentRegistry = {
286 let reg = Arc::new(IdempotentRegistry::new());
287 let memory = Arc::new(MemoryIdempotentRepository::new("memory"));
288 reg.register("memory", memory)
291 .expect("built-in memory idempotent repository registration must succeed"); reg
293 };
294
295 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"); reg
302 };
303
304 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"); reg
311 };
312
313 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(®istry),
326 Arc::clone(&languages),
327 Arc::clone(&platform_service),
328 Arc::clone(beans),
329 )
330 } else {
331 DefaultRouteController::with_languages(
332 Arc::clone(®istry),
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(®istry),
363 Arc::clone(&languages),
364 Arc::clone(&platform_service),
365 Arc::clone(beans),
366 )
367 } else {
368 DefaultRouteController::with_languages(
369 Arc::clone(®istry),
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"); 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}