camel_component_sql/
pool_factory.rs1use std::any::Any;
2use std::sync::Arc;
3use std::time::Duration;
4
5use camel_api::datasource::{CheckFuture, CloseFuture, CreatePoolFuture};
6use camel_api::datasource::{DatasourceConfig, DatasourceHandle, PoolFactory};
7use camel_api::error::CamelError;
8use camel_api::lifecycle::HealthStatus;
9use sqlx::AnyPool;
10use sqlx::any::AnyPoolOptions;
11
12use crate::config::{enrich_db_url_with_ssl_params, redact_db_url};
13
14fn is_sqlite_memory_url(url: &str) -> bool {
18 let lowered = url.to_lowercase();
19 lowered.starts_with("sqlite::memory:")
20 || lowered.starts_with("sqlite://:memory:")
21 || (lowered.starts_with("sqlite:") && lowered.contains("mode=memory"))
22}
23
24const IN_FLIGHT_DRAIN_WAIT: Duration = Duration::from_secs(10);
28const IN_FLIGHT_DRAIN_POLL: Duration = Duration::from_millis(5);
30
31pub struct SqlPoolFactory;
32
33impl PoolFactory for SqlPoolFactory {
34 fn create<'a>(&'a self, config: &'a DatasourceConfig) -> CreatePoolFuture<'a> {
35 Box::pin(async move {
36 sqlx::any::install_default_drivers();
39
40 let max_conn = config.max_connections.unwrap_or(5);
41 let min_conn = if is_sqlite_memory_url(&config.db_url) {
50 if config.min_connections.is_some_and(|m| m > 0) {
51 tracing::info!("datasource pool: min_connections ignored for in-memory sqlite");
53 }
54 0
55 } else {
56 config.min_connections.unwrap_or(1)
57 };
58 let idle_timeout = Duration::from_secs(config.idle_timeout_secs.unwrap_or(300));
59 let max_lifetime = Duration::from_secs(config.max_lifetime_secs.unwrap_or(1800));
60
61 let db_url = enrich_db_url_with_ssl_params(
62 &config.db_url,
63 config.ssl_mode.as_deref(),
64 config.ssl_root_cert.as_deref(),
65 config.ssl_cert.as_deref(),
66 config.ssl_key.as_deref(),
67 )?;
68
69 let pool = AnyPoolOptions::new()
70 .max_connections(max_conn)
71 .min_connections(min_conn)
72 .idle_timeout(idle_timeout)
73 .max_lifetime(max_lifetime)
74 .connect(&db_url)
75 .await
76 .map_err(|e| {
77 CamelError::ProcessorError(format!(
78 "failed to create datasource pool ({}): {}",
79 redact_db_url(&config.db_url),
80 e
81 ))
82 })?;
83
84 tracing::info!("datasource pool created: max_connections={}", max_conn);
85 Ok(Arc::new(pool) as Arc<dyn Any + Send + Sync>)
86 })
87 }
88
89 fn check<'a>(&'a self, handle: &'a DatasourceHandle) -> CheckFuture<'a> {
90 Box::pin(async move {
91 match handle.downcast::<AnyPool>() {
92 Ok(pool) => match sqlx::query("SELECT 1").execute(&*pool).await {
93 Ok(_) => HealthStatus::Healthy,
94 Err(e) => {
95 tracing::warn!("datasource '{}' health check failed: {}", handle.name, e);
97 HealthStatus::Unhealthy
98 }
99 },
100 Err(e) => {
101 tracing::warn!(
103 "datasource '{}' health check failed: pool downcast error: {}",
104 handle.name,
105 e
106 );
107 HealthStatus::Unhealthy
108 }
109 }
110 })
111 }
112
113 fn close<'a>(&'a self, handle: &'a DatasourceHandle) -> CloseFuture<'a> {
114 Box::pin(async move {
115 let pool = handle.downcast::<AnyPool>().map_err(|e| {
116 CamelError::ProcessorError(format!(
117 "datasource '{}': pool close downcast failed: {}",
118 handle.name, e
119 ))
120 })?;
121 pool.close().await;
124 let drain_deadline = std::time::Instant::now() + IN_FLIGHT_DRAIN_WAIT;
138 while pool.size() > 0 {
139 if std::time::Instant::now() >= drain_deadline {
140 return Err(CamelError::ProcessorError(format!(
148 "datasource '{}': pool did not drain within {}s ({} connection(s) \
149 still open) — the database may outlive its boot",
150 handle.name,
151 IN_FLIGHT_DRAIN_WAIT.as_secs(),
152 pool.size()
153 )));
154 }
155 tokio::time::sleep(IN_FLIGHT_DRAIN_POLL).await;
161 pool.close().await;
162 }
163 Ok(())
164 })
165 }
166
167 fn supported_schemes(&self) -> &[&str] {
168 &["postgres", "postgresql", "mysql", "sqlite"]
169 }
170
171 fn name(&self) -> &'static str {
172 "sqlx"
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn sql_pool_factory_name() {
182 let f = SqlPoolFactory;
183 assert_eq!(f.name(), "sqlx");
184 }
185
186 #[test]
187 fn sql_pool_factory_supported_schemes() {
188 let f = SqlPoolFactory;
189 assert!(f.supported_schemes().contains(&"postgres"));
190 assert!(f.supported_schemes().contains(&"mysql"));
191 assert!(f.supported_schemes().contains(&"sqlite"));
192 }
193
194 #[test]
195 fn sql_pool_factory_matches_postgres() {
196 let f = SqlPoolFactory;
197 let cfg = DatasourceConfig {
198 db_url: "postgres://localhost/test".into(),
199 provider: None,
200 max_connections: None,
201 min_connections: None,
202 idle_timeout_secs: None,
203 max_lifetime_secs: None,
204 ssl_mode: None,
205 ssl_root_cert: None,
206 ssl_cert: None,
207 ssl_key: None,
208 extra: std::collections::HashMap::new(),
209 };
210 assert!(f.matches(&cfg));
211 }
212
213 #[tokio::test]
214 async fn sql_pool_factory_close_closes_the_pool() {
215 let f = SqlPoolFactory;
216 let cfg = DatasourceConfig {
217 db_url: "sqlite::memory:?cache=shared".into(),
218 provider: None,
219 max_connections: None,
220 min_connections: None,
221 idle_timeout_secs: None,
222 max_lifetime_secs: None,
223 ssl_mode: None,
224 ssl_root_cert: None,
225 ssl_cert: None,
226 ssl_key: None,
227 extra: std::collections::HashMap::new(),
228 };
229 let inner = f.create(&cfg).await.unwrap();
230 let pool = Arc::downcast::<AnyPool>(Arc::clone(&inner)).unwrap();
231 let handle = DatasourceHandle::new("appdb".into(), f.name().into(), Arc::clone(&inner));
232
233 f.close(&handle).await.unwrap();
234 assert!(
235 pool.is_closed(),
236 "factory close must drain the sqlx pool (bd rc-25lup.4)"
237 );
238 }
239
240 #[tokio::test]
245 async fn named_shared_memory_uri_probe() {
246 use sqlx::Row;
247
248 let f = SqlPoolFactory;
249 let cfg = DatasourceConfig {
250 db_url: "sqlite:file:memdb_probe?mode=memory&cache=shared".into(),
251 provider: None,
252 max_connections: Some(3),
253 min_connections: None,
254 idle_timeout_secs: None,
255 max_lifetime_secs: None,
256 ssl_mode: None,
257 ssl_root_cert: None,
258 ssl_cert: None,
259 ssl_key: None,
260 extra: std::collections::HashMap::new(),
261 };
262 let inner = f.create(&cfg).await.unwrap();
263 let pool = Arc::downcast::<AnyPool>(Arc::clone(&inner)).unwrap();
264
265 sqlx::query("CREATE TABLE probe (v TEXT)")
266 .execute(&*pool)
267 .await
268 .expect("create");
269 let conn1 = pool.acquire().await.expect("conn1");
272 sqlx::query("INSERT INTO probe VALUES ('x')")
273 .execute(&*pool)
274 .await
275 .expect("insert on a second connection");
276 drop(conn1);
277
278 let row = sqlx::query("SELECT COUNT(*) FROM probe")
279 .fetch_one(&*pool)
280 .await
281 .expect("count");
282 let n = row.try_get::<i64, usize>(0).expect("count i64");
283 assert_eq!(
284 n, 1,
285 "named shared memory URI must share across pool connections"
286 );
287 }
288}