Skip to main content

camel_core/
datasource.rs

1//! # datasource โ€” single-ring adapter module
2//!
3//! Runtime catalog of datasource pools keyed by name, with a `PoolFactory`
4//! registry and lazy `OnceCell` pool initialization.
5//!
6//! ## Ring classification (ADR-0045 ยง5)
7//!
8//! Single-ring **Interface Adapters** module. `RuntimeDatasourceCatalog` holds
9//! `dashmap::DashMap`, `tokio::sync::OnceCell`, `parking_lot::RwLock` fields
10//! and orchestrates framework I/O via `get_pool`/`resolve_factory`. Earlier
11//! 3-ring split mislabeled the stateful catalog as "domain"; this single-file
12//! adapter form corrects the label so the dependency rule holds.
13
14use 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
26// === Value types (formerly domain.rs) ===
27
28type CacheKey = (String, String);
29
30// === Aggregate root ===
31
32pub 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
71// === Use-case orchestration (formerly application.rs) ===
72
73impl 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(); // allow-unwrap (parking_lot panics on poison)
84        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            // Collect first: DashMap entry guards are not Send, and the
143            // returned future must be. Uninitialized cells never opened a
144            // pool โ€” nothing to close.
145            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                // `DatasourceHandle::provider` carries the FACTORY NAME
154                // (`factory.name()`), but the registry key is an
155                // arbitrary `kind` string โ€” a factory registered as
156                // ("postgresql", name "pg") must still close. Resolve by
157                // name match over the registered values, never by key.
158                let factory = {
159                    let factories = self.factories().read(); // allow-unwrap (parking_lot panics on poison)
160                    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                            // log-policy: outside-contract
169                            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                        // log-policy: outside-contract
177                        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(); // allow-unwrap (parking_lot panics on poison)
197        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
227/// Extract the scheme portion of a database URL for safe display
228/// without leaking credentials.
229fn 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
259// === External trait delegation (formerly adapters.rs) ===
260
261use 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// === Tests ===
286
287#[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}