1use std::collections::HashMap;
15use std::sync::Arc;
16
17use camel_api::datasource::{DatasourceConfig, DatasourceHandle, PoolFactory};
18use camel_api::error::CamelError;
19use camel_api::health::{AsyncHealthCheck, CheckResult};
20use dashmap::DashMap;
21use parking_lot::RwLock;
22use tokio::sync::OnceCell;
23
24use crate::health_registry::HealthCheckRegistry;
25
26type CacheKey = (String, String);
29
30pub struct RuntimeDatasourceCatalog {
33 configs: HashMap<String, DatasourceConfig>,
34 factories: RwLock<HashMap<String, Arc<dyn PoolFactory>>>,
35 pools: DashMap<CacheKey, Arc<OnceCell<DatasourceHandle>>>,
36 health_registry: Option<Arc<HealthCheckRegistry>>,
37}
38
39impl RuntimeDatasourceCatalog {
40 pub fn new(configs: HashMap<String, DatasourceConfig>) -> Self {
41 Self {
42 configs,
43 factories: RwLock::new(HashMap::new()),
44 pools: DashMap::new(),
45 health_registry: None,
46 }
47 }
48
49 pub fn with_health_registry(mut self, registry: Arc<HealthCheckRegistry>) -> Self {
50 self.health_registry = Some(registry);
51 self
52 }
53
54 fn configs(&self) -> &HashMap<String, DatasourceConfig> {
55 &self.configs
56 }
57
58 fn pools(&self) -> &DashMap<CacheKey, Arc<OnceCell<DatasourceHandle>>> {
59 &self.pools
60 }
61
62 fn health_registry(&self) -> Option<&Arc<HealthCheckRegistry>> {
63 self.health_registry.as_ref()
64 }
65
66 fn factories(&self) -> &RwLock<HashMap<String, Arc<dyn PoolFactory>>> {
67 &self.factories
68 }
69}
70
71impl RuntimeDatasourceCatalog {
74 pub(crate) fn get_config(&self, name: &str) -> Option<DatasourceConfig> {
75 self.configs().get(name).cloned()
76 }
77
78 pub(crate) fn register_factory(
79 &self,
80 kind: &str,
81 factory: Arc<dyn PoolFactory>,
82 ) -> Result<(), CamelError> {
83 let mut factories = self.factories().write(); if factories.contains_key(kind) {
85 return Err(CamelError::Config(format!(
86 "factory '{}' already registered",
87 kind
88 )));
89 }
90 factories.insert(kind.to_string(), factory);
91 Ok(())
92 }
93
94 pub(crate) fn get_pool<'a>(
95 &'a self,
96 name: &'a str,
97 ) -> camel_api::datasource::GetPoolFuture<'a> {
98 Box::pin(async move {
99 let config = self.configs().get(name).cloned().ok_or_else(|| {
100 CamelError::Config(format!("datasource '{}' not found in catalog", name))
101 })?;
102
103 let factory = self.resolve_factory(&config)?;
104 let cache_key: CacheKey = (name.to_string(), factory.name().to_string());
105
106 let cell = self
107 .pools()
108 .entry(cache_key)
109 .or_insert_with(|| Arc::new(OnceCell::new()))
110 .clone();
111
112 let handle = cell
113 .get_or_try_init(|| async {
114 let inner = factory.create(&config).await?;
115 let handle =
116 DatasourceHandle::new(name.to_string(), factory.name().to_string(), inner);
117
118 if let Some(registry) = self.health_registry() {
119 let factory_ref = factory.clone();
120 let handle_for_check = handle.clone();
121 let ds_name = name.to_string();
122 registry.register_for_route(
123 &format!("datasource:{}", ds_name),
124 std::sync::Arc::new(DatasourceHealthCheck {
125 check_name: format!("datasource:{}", ds_name),
126 factory: factory_ref,
127 handle: handle_for_check,
128 }),
129 );
130 registry.mark_route_started(&format!("datasource:{}", ds_name));
131 }
132
133 Ok::<DatasourceHandle, CamelError>(handle)
134 })
135 .await?;
136 Ok(handle.clone())
137 })
138 }
139
140 pub(crate) fn close_all(&self) -> camel_api::datasource::CloseAllFuture<'_> {
141 Box::pin(async move {
142 let handles: Vec<DatasourceHandle> = self
146 .pools()
147 .iter()
148 .filter_map(|entry| entry.value().get().cloned())
149 .collect();
150
151 let mut first_failure: Option<CamelError> = None;
152 for handle in &handles {
153 let factory = {
159 let factories = self.factories().read(); factories
161 .values()
162 .find(|f| f.name() == handle.provider)
163 .cloned()
164 };
165 match factory {
166 Some(factory) => {
167 if let Err(e) = factory.close(handle).await {
168 tracing::warn!("datasource '{}' pool close failed: {}", handle.name, e);
170 if first_failure.is_none() {
171 first_failure = Some(e);
172 }
173 }
174 }
175 None => {
176 tracing::warn!(
178 "datasource '{}' close skipped: factory '{}' not registered",
179 handle.name,
180 handle.provider
181 );
182 }
183 }
184 }
185 match first_failure {
186 Some(e) => Err(e),
187 None => Ok(()),
188 }
189 })
190 }
191
192 fn resolve_factory(
193 &self,
194 config: &DatasourceConfig,
195 ) -> Result<Arc<dyn PoolFactory>, CamelError> {
196 let factories = self.factories().read(); if let Some(ref provider) = config.provider {
198 let factory = factories.get(provider).ok_or_else(|| {
199 CamelError::Config(format!("unknown datasource provider '{}'", provider))
200 })?;
201 return Ok(factory.clone());
202 }
203
204 let matches: Vec<_> = factories
205 .values()
206 .filter(|entry| entry.matches(config))
207 .collect();
208
209 match matches.len() {
210 0 => Err(CamelError::Config(format!(
211 "no matching factory for datasource url '{}'",
212 scheme_hint(&config.db_url)
213 ))),
214 1 => Ok(matches[0].clone()),
215 _ => {
216 let names: Vec<_> = matches.iter().map(|m| m.name()).collect();
217 Err(CamelError::Config(format!(
218 "ambiguous datasource: {} factories match '{}'. Set explicit 'provider' field.",
219 names.len(),
220 scheme_hint(&config.db_url)
221 )))
222 }
223 }
224 }
225}
226
227fn scheme_hint(db_url: &str) -> String {
230 if let Some(scheme_end) = db_url.find("://") {
231 format!("{}://...", &db_url[..scheme_end])
232 } else {
233 "[REDACTED]".to_string()
234 }
235}
236
237struct DatasourceHealthCheck {
238 check_name: String,
239 factory: Arc<dyn PoolFactory>,
240 handle: DatasourceHandle,
241}
242
243#[async_trait::async_trait]
244impl AsyncHealthCheck for DatasourceHealthCheck {
245 fn name(&self) -> &str {
246 &self.check_name
247 }
248
249 async fn check(&self) -> CheckResult {
250 let status = self.factory.check(&self.handle).await;
251 CheckResult {
252 name: self.check_name.clone(),
253 status,
254 message: None,
255 }
256 }
257}
258
259use camel_api::datasource::DatasourceCatalog;
262
263impl DatasourceCatalog for RuntimeDatasourceCatalog {
264 fn get_config(&self, name: &str) -> Option<DatasourceConfig> {
265 self.get_config(name)
266 }
267
268 fn get_pool<'a>(&'a self, name: &'a str) -> camel_api::datasource::GetPoolFuture<'a> {
269 self.get_pool(name)
270 }
271
272 fn register_factory(
273 &self,
274 kind: &str,
275 factory: Arc<dyn PoolFactory>,
276 ) -> Result<(), CamelError> {
277 self.register_factory(kind, factory)
278 }
279
280 fn close_all(&self) -> camel_api::datasource::CloseAllFuture<'_> {
281 self.close_all()
282 }
283}
284
285#[cfg(test)]
288mod tests {
289 use super::*;
290 use std::any::Any;
291 use std::sync::atomic::{AtomicUsize, Ordering};
292
293 use camel_api::datasource::{CheckFuture, CreatePoolFuture};
294 use camel_api::lifecycle::HealthStatus;
295
296 struct MockFactory {
297 name: &'static str,
298 schemes: &'static [&'static str],
299 create_count: Arc<AtomicUsize>,
300 }
301
302 impl PoolFactory for MockFactory {
303 fn create<'a>(&'a self, _config: &'a DatasourceConfig) -> CreatePoolFuture<'a> {
304 let count = self.create_count.clone();
305 Box::pin(async move {
306 count.fetch_add(1, Ordering::SeqCst);
307 Ok(Arc::new("mock_pool") as Arc<dyn Any + Send + Sync>)
308 })
309 }
310
311 fn check<'a>(&'a self, _handle: &'a DatasourceHandle) -> CheckFuture<'a> {
312 Box::pin(async { HealthStatus::Healthy })
313 }
314
315 fn supported_schemes(&self) -> &[&str] {
316 self.schemes
317 }
318
319 fn name(&self) -> &'static str {
320 self.name
321 }
322 }
323
324 fn make_config(db_url: &str) -> DatasourceConfig {
325 DatasourceConfig {
326 db_url: db_url.to_string(),
327 provider: None,
328 max_connections: None,
329 min_connections: None,
330 idle_timeout_secs: None,
331 max_lifetime_secs: None,
332 ssl_mode: None,
333 ssl_root_cert: None,
334 ssl_cert: None,
335 ssl_key: None,
336 extra: std::collections::HashMap::new(),
337 }
338 }
339
340 #[tokio::test]
341 async fn register_factory_and_get_pool() {
342 let mut configs = HashMap::new();
343 configs.insert(
344 "mydb".to_string(),
345 make_config("postgresql://localhost/mydb"),
346 );
347
348 let catalog = RuntimeDatasourceCatalog::new(configs);
349 let factory = Arc::new(MockFactory {
350 name: "pg",
351 schemes: &["postgresql", "postgres"],
352 create_count: Arc::new(AtomicUsize::new(0)),
353 });
354 catalog.register_factory("postgresql", factory).unwrap();
355
356 let handle = catalog.get_pool("mydb").await.unwrap();
357 assert_eq!(handle.name, "mydb");
358 assert_eq!(handle.provider, "pg");
359 }
360
361 #[tokio::test]
362 async fn shared_pool_for_same_datasource() {
363 let mut configs = HashMap::new();
364 configs.insert(
365 "mydb".to_string(),
366 make_config("postgresql://localhost/mydb"),
367 );
368
369 let count = Arc::new(AtomicUsize::new(0));
370 let catalog = RuntimeDatasourceCatalog::new(configs);
371 let factory = Arc::new(MockFactory {
372 name: "pg",
373 schemes: &["postgresql", "postgres"],
374 create_count: count.clone(),
375 });
376 catalog.register_factory("postgresql", factory).unwrap();
377
378 let h1 = catalog.get_pool("mydb").await.unwrap();
379 let h2 = catalog.get_pool("mydb").await.unwrap();
380
381 assert_eq!(h1.name, h2.name);
382 assert_eq!(h1.provider, h2.provider);
383 assert_eq!(count.load(Ordering::SeqCst), 1);
384 }
385
386 #[tokio::test]
387 async fn unknown_datasource_returns_error() {
388 let configs = HashMap::new();
389 let catalog = RuntimeDatasourceCatalog::new(configs);
390
391 let result = catalog.get_pool("nonexistent").await;
392 assert!(result.is_err());
393 let err = result.unwrap_err();
394 assert!(err.to_string().contains("not found"));
395 }
396
397 #[tokio::test]
398 async fn duplicate_factory_returns_error() {
399 let configs = HashMap::new();
400 let catalog = RuntimeDatasourceCatalog::new(configs);
401 let factory = Arc::new(MockFactory {
402 name: "pg",
403 schemes: &["postgresql"],
404 create_count: Arc::new(AtomicUsize::new(0)),
405 });
406
407 catalog.register_factory("pg", factory.clone()).unwrap();
408 let result = catalog.register_factory("pg", factory);
409 assert!(result.is_err());
410 let err = result.unwrap_err();
411 assert!(err.to_string().contains("already registered"));
412 }
413
414 #[tokio::test]
415 async fn no_matching_factory_returns_error() {
416 let mut configs = HashMap::new();
417 configs.insert("mydb".to_string(), make_config("mongodb://localhost/mydb"));
418
419 let catalog = RuntimeDatasourceCatalog::new(configs);
420 let factory = Arc::new(MockFactory {
421 name: "pg",
422 schemes: &["postgresql"],
423 create_count: Arc::new(AtomicUsize::new(0)),
424 });
425 catalog.register_factory("postgresql", factory).unwrap();
426
427 let result = catalog.get_pool("mydb").await;
428 assert!(result.is_err());
429 let err = result.unwrap_err();
430 assert!(err.to_string().contains("no matching factory"));
431 }
432
433 #[tokio::test]
434 async fn explicit_provider_overrides_scheme() {
435 let mut configs = HashMap::new();
436 configs.insert(
437 "mydb".to_string(),
438 DatasourceConfig {
439 db_url: "postgresql://localhost/mydb".to_string(),
440 provider: Some("mysql_factory".to_string()),
441 max_connections: None,
442 min_connections: None,
443 idle_timeout_secs: None,
444 max_lifetime_secs: None,
445 ssl_mode: None,
446 ssl_root_cert: None,
447 ssl_cert: None,
448 ssl_key: None,
449 extra: std::collections::HashMap::new(),
450 },
451 );
452
453 let pg_count = Arc::new(AtomicUsize::new(0));
454 let mysql_count = Arc::new(AtomicUsize::new(0));
455
456 let catalog = RuntimeDatasourceCatalog::new(configs);
457 let pg_factory = Arc::new(MockFactory {
458 name: "pg",
459 schemes: &["postgresql"],
460 create_count: pg_count.clone(),
461 });
462 let mysql_factory = Arc::new(MockFactory {
463 name: "mysql_factory",
464 schemes: &["mysql"],
465 create_count: mysql_count.clone(),
466 });
467
468 catalog.register_factory("postgresql", pg_factory).unwrap();
469 catalog
470 .register_factory("mysql_factory", mysql_factory)
471 .unwrap();
472
473 let handle = catalog.get_pool("mydb").await.unwrap();
474 assert_eq!(handle.provider, "mysql_factory");
475 assert_eq!(pg_count.load(Ordering::SeqCst), 0);
476 assert_eq!(mysql_count.load(Ordering::SeqCst), 1);
477 }
478
479 #[tokio::test]
480 async fn get_config_returns_clone() {
481 let mut configs = HashMap::new();
482 let original = make_config("postgresql://localhost/mydb");
483 configs.insert("mydb".to_string(), original.clone());
484
485 let catalog = RuntimeDatasourceCatalog::new(configs);
486 let retrieved = catalog.get_config("mydb");
487 assert!(retrieved.is_some());
488 assert_eq!(retrieved.unwrap().db_url, original.db_url);
489 }
490
491 #[tokio::test]
492 async fn get_pool_before_factory_registered_returns_clear_error() {
493 let mut configs = HashMap::new();
494 configs.insert(
495 "mydb".to_string(),
496 make_config("postgresql://localhost/mydb"),
497 );
498
499 let catalog = RuntimeDatasourceCatalog::new(configs);
500
501 let result = catalog.get_pool("mydb").await;
502 assert!(result.is_err());
503 let err = result.unwrap_err();
504 assert!(err.to_string().contains("no matching factory"));
505 }
506
507 #[tokio::test]
508 async fn ambiguous_factory_returns_error() {
509 let mut configs = HashMap::new();
510 configs.insert("orders".into(), make_config("postgres://localhost/test"));
511 let catalog = RuntimeDatasourceCatalog::new(configs);
512 catalog
513 .register_factory(
514 "mock1",
515 Arc::new(MockFactory {
516 name: "mock1",
517 schemes: &["postgres"],
518 create_count: Arc::new(AtomicUsize::new(0)),
519 }),
520 )
521 .unwrap();
522
523 struct MockFactory2;
524 impl PoolFactory for MockFactory2 {
525 fn create<'a>(&'a self, config: &'a DatasourceConfig) -> CreatePoolFuture<'a> {
526 Box::pin(async move {
527 Ok(Arc::new(config.db_url.clone()) as Arc<dyn Any + Send + Sync>)
528 })
529 }
530 fn check<'a>(&'a self, _handle: &'a DatasourceHandle) -> CheckFuture<'a> {
531 Box::pin(async { HealthStatus::Healthy })
532 }
533 fn supported_schemes(&self) -> &[&str] {
534 &["postgres"]
535 }
536 fn name(&self) -> &'static str {
537 "mock2"
538 }
539 }
540 catalog
541 .register_factory("mock2", Arc::new(MockFactory2))
542 .unwrap();
543
544 let result = catalog.get_pool("orders").await;
545 assert!(result.is_err());
546 let msg = result.unwrap_err().to_string();
547 assert!(
548 msg.contains("ambiguous"),
549 "expected ambiguous error, got: {}",
550 msg
551 );
552 }
553
554 #[tokio::test]
555 async fn bad_downcast_returns_clear_error() {
556 let mut configs = HashMap::new();
557 configs.insert(
558 "mydb".to_string(),
559 make_config("postgresql://localhost/mydb"),
560 );
561
562 let catalog = RuntimeDatasourceCatalog::new(configs);
563 let factory = Arc::new(MockFactory {
564 name: "pg",
565 schemes: &["postgresql"],
566 create_count: Arc::new(AtomicUsize::new(0)),
567 });
568 catalog.register_factory("postgresql", factory).unwrap();
569
570 let handle = catalog.get_pool("mydb").await.unwrap();
571
572 let result: Result<Arc<String>, CamelError> = handle.downcast();
573 assert!(result.is_err());
574 let err = result.unwrap_err();
575 assert!(err.to_string().contains("failed to downcast"));
576 assert!(err.to_string().contains("mydb"));
577 assert!(err.to_string().contains("pg"));
578 }
579
580 #[tokio::test]
581 async fn health_check_registered_after_pool_creation() {
582 let mut configs = HashMap::new();
583 configs.insert(
584 "orders".to_string(),
585 make_config("postgresql://localhost/orders"),
586 );
587
588 let registry = Arc::new(crate::health_registry::HealthCheckRegistry::new(
589 std::time::Duration::from_secs(5),
590 ));
591 let catalog = RuntimeDatasourceCatalog::new(configs).with_health_registry(registry.clone());
592 catalog
593 .register_factory(
594 "postgresql",
595 Arc::new(MockFactory {
596 name: "pg",
597 schemes: &["postgresql", "postgres"],
598 create_count: Arc::new(AtomicUsize::new(0)),
599 }),
600 )
601 .unwrap();
602
603 let _ = catalog.get_pool("orders").await.unwrap();
604 registry.mark_route_started("datasource:orders");
605
606 let report = registry.check_all().await;
607 assert!(
608 report
609 .services
610 .iter()
611 .any(|s| s.name.starts_with("datasource:")),
612 "expected datasource health check in report, got: {:?}",
613 report.services
614 );
615 }
616}