1use std::fmt::Debug;
6use std::ops::{Deref, DerefMut};
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Mutex};
9use std::time::Duration;
10
11#[cfg(feature = "prometheus")]
12use cdk_prometheus::metrics::METRICS;
13use tokio::sync::{OwnedSemaphorePermit, Semaphore};
14
15use crate::database::DatabaseConnector;
16
17#[derive(Debug, thiserror::Error)]
19pub enum Error<E>
20where
21 E: std::error::Error + Send + Sync + 'static,
22{
23 #[error("Internal: PoisonError")]
25 Poison,
26
27 #[error("Timed out waiting for a resource")]
29 Timeout,
30
31 #[error(transparent)]
33 Resource(#[from] E),
34}
35
36pub trait DatabaseConfig: Clone + Debug + Send + Sync {
38 fn max_size(&self) -> usize;
40
41 fn default_timeout(&self) -> Duration;
43}
44
45pub trait DatabasePool: Debug {
47 type Connection: DatabaseConnector;
49
50 type Config: DatabaseConfig;
52
53 type Error: Debug + std::error::Error + Send + Sync + 'static;
55
56 fn new_resource(
61 config: &Self::Config,
62 stale: Arc<AtomicBool>,
63 timeout: Duration,
64 ) -> Result<Self::Connection, Error<Self::Error>>;
65
66 fn drop(_resource: Self::Connection) {}
68}
69
70pub struct Pool<RM>
72where
73 RM: DatabasePool,
74{
75 config: RM::Config,
76 queue: Mutex<Vec<(Arc<AtomicBool>, RM::Connection)>>,
77 max_size: usize,
78 default_timeout: Duration,
79 semaphore: Arc<Semaphore>,
80}
81
82impl<RM> Debug for Pool<RM>
83where
84 RM: DatabasePool,
85{
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("Pool")
88 .field("config", &self.config)
89 .field("max_size", &self.max_size)
90 .field("default_timeout", &self.default_timeout)
91 .field("available_permits", &self.semaphore.available_permits())
92 .finish()
93 }
94}
95
96pub struct PooledResource<RM>
98where
99 RM: DatabasePool,
100{
101 resource: Option<(Arc<AtomicBool>, RM::Connection)>,
102 pool: Arc<Pool<RM>>,
103 _permit: OwnedSemaphorePermit,
104 #[cfg(feature = "prometheus")]
105 start_time: std::time::Instant,
106}
107
108impl<RM> PooledResource<RM>
109where
110 RM: DatabasePool,
111{
112 pub(crate) fn stale(&self) -> Arc<AtomicBool> {
113 self.resource
114 .as_ref()
115 .expect("resource already dropped")
116 .0
117 .clone()
118 }
119}
120
121impl<RM> Debug for PooledResource<RM>
122where
123 RM: DatabasePool,
124{
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "Resource: {:?}", self.resource)
127 }
128}
129
130impl<RM> Drop for PooledResource<RM>
131where
132 RM: DatabasePool,
133{
134 fn drop(&mut self) {
135 if let Some((stale, resource)) = self.resource.take() {
136 if stale.load(Ordering::SeqCst) {
137 RM::drop(resource);
138 } else {
139 let mut active_resource = self.pool.queue.lock().expect("active_resource");
140 active_resource.push((stale, resource));
141 }
142
143 #[cfg(feature = "prometheus")]
144 {
145 METRICS.decrement_db_connections_active();
146
147 let duration = self.start_time.elapsed().as_secs_f64();
148
149 METRICS.record_db_operation(duration, "drop");
150 }
151
152 }
155 }
156}
157
158impl<RM> Deref for PooledResource<RM>
159where
160 RM: DatabasePool,
161{
162 type Target = RM::Connection;
163
164 fn deref(&self) -> &Self::Target {
165 &self.resource.as_ref().expect("resource already dropped").1
166 }
167}
168
169impl<RM> DerefMut for PooledResource<RM>
170where
171 RM: DatabasePool,
172{
173 fn deref_mut(&mut self) -> &mut Self::Target {
174 &mut self.resource.as_mut().expect("resource already dropped").1
175 }
176}
177
178impl<RM> Pool<RM>
179where
180 RM: DatabasePool,
181{
182 pub fn new(config: RM::Config) -> Arc<Self> {
184 let max_size = config.max_size();
185 Arc::new(Self {
186 default_timeout: config.default_timeout(),
187 max_size,
188 config,
189 queue: Default::default(),
190 semaphore: Arc::new(Semaphore::new(max_size)),
191 })
192 }
193
194 #[inline(always)]
196 pub async fn get(self: &Arc<Self>) -> Result<PooledResource<RM>, Error<RM::Error>> {
197 self.get_timeout(self.default_timeout).await
198 }
199
200 #[inline(always)]
205 pub async fn get_timeout(
206 self: &Arc<Self>,
207 timeout: Duration,
208 ) -> Result<PooledResource<RM>, Error<RM::Error>> {
209 let permit = match self.semaphore.clone().try_acquire_owned() {
211 Ok(permit) => permit,
212 Err(tokio::sync::TryAcquireError::Closed) => return Err(Error::Poison),
213 Err(tokio::sync::TryAcquireError::NoPermits) => {
214 tracing::debug!(
218 "Pool exhausted (size: {}), waiting for a connection",
219 self.max_size,
220 );
221 tokio::time::timeout(timeout, self.semaphore.clone().acquire_owned())
222 .await
223 .map_err(|_| Error::Timeout)?
224 .map_err(|_| Error::Poison)?
225 }
226 };
227
228 #[cfg(feature = "prometheus")]
229 METRICS.increment_db_connections_active();
230
231 {
234 let mut resources = match self.queue.lock() {
235 Ok(resources) => resources,
236 Err(_) => {
237 #[cfg(feature = "prometheus")]
238 METRICS.decrement_db_connections_active();
239 return Err(Error::Poison);
240 }
241 };
242 while let Some((stale, resource)) = resources.pop() {
243 if !stale.load(Ordering::SeqCst) {
244 return Ok(PooledResource {
245 resource: Some((stale, resource)),
246 pool: self.clone(),
247 _permit: permit,
248 #[cfg(feature = "prometheus")]
249 start_time: std::time::Instant::now(),
250 });
251 }
252 }
254 }
255
256 let stale: Arc<AtomicBool> = Arc::new(false.into());
259 match RM::new_resource(&self.config, stale.clone(), timeout) {
260 Ok(new_resource) => Ok(PooledResource {
261 resource: Some((stale, new_resource)),
262 pool: self.clone(),
263 _permit: permit,
264 #[cfg(feature = "prometheus")]
265 start_time: std::time::Instant::now(),
266 }),
267 Err(e) => {
268 #[cfg(feature = "prometheus")]
269 METRICS.decrement_db_connections_active();
270
271 Err(e)
273 }
274 }
275 }
276}
277
278impl<RM> Drop for Pool<RM>
279where
280 RM: DatabasePool,
281{
282 fn drop(&mut self) {
283 self.semaphore.close();
285
286 if let Ok(mut resources) = self.queue.lock() {
288 while let Some(resource) = resources.pop() {
289 RM::drop(resource.1);
290 }
291 }
292 }
293}
294
295#[cfg(all(test, feature = "prometheus"))]
296mod tests {
297 use std::fmt;
298 use std::sync::atomic::AtomicBool;
299 use std::sync::Arc;
300 use std::time::Duration;
301
302 use cdk_common::database::Error as DatabaseError;
303 use cdk_prometheus::METRICS;
304
305 use super::{DatabaseConfig, DatabasePool, Error, Pool};
306 use crate::database::{DatabaseConnector, DatabaseExecutor, DatabaseTransaction};
307 use crate::stmt::{Column, Statement};
308
309 #[derive(Debug, Clone)]
310 struct TestConfig {
311 max_size: usize,
312 default_timeout: Duration,
313 fail_new_resource: bool,
314 }
315
316 impl DatabaseConfig for TestConfig {
317 fn max_size(&self) -> usize {
318 self.max_size
319 }
320
321 fn default_timeout(&self) -> Duration {
322 self.default_timeout
323 }
324 }
325
326 #[derive(Debug)]
327 struct TestResourceError;
328
329 impl fmt::Display for TestResourceError {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 f.write_str("test resource error")
332 }
333 }
334
335 impl std::error::Error for TestResourceError {}
336
337 #[derive(Debug)]
338 struct TestConnection;
339
340 #[async_trait::async_trait]
341 impl DatabaseExecutor for TestConnection {
342 fn name() -> &'static str {
343 "test"
344 }
345
346 async fn execute(&self, _statement: Statement) -> Result<usize, DatabaseError> {
347 Ok(0)
348 }
349
350 async fn fetch_one(
351 &self,
352 _statement: Statement,
353 ) -> Result<Option<Vec<Column>>, DatabaseError> {
354 Ok(None)
355 }
356
357 async fn fetch_all(
358 &self,
359 _statement: Statement,
360 ) -> Result<Vec<Vec<Column>>, DatabaseError> {
361 Ok(Vec::new())
362 }
363
364 async fn pluck(&self, _statement: Statement) -> Result<Option<Column>, DatabaseError> {
365 Ok(None)
366 }
367
368 async fn batch(&self, _statement: Statement) -> Result<(), DatabaseError> {
369 Ok(())
370 }
371 }
372
373 #[derive(Debug)]
374 struct TestTransaction;
375
376 #[async_trait::async_trait]
377 impl DatabaseTransaction<TestConnection> for TestTransaction {
378 async fn commit(_conn: &mut TestConnection) -> Result<(), DatabaseError> {
379 Ok(())
380 }
381
382 async fn begin(_conn: &mut TestConnection) -> Result<(), DatabaseError> {
383 Ok(())
384 }
385
386 async fn rollback(_conn: &mut TestConnection) -> Result<(), DatabaseError> {
387 Ok(())
388 }
389 }
390
391 impl DatabaseConnector for TestConnection {
392 type Transaction = TestTransaction;
393 }
394
395 #[derive(Debug)]
396 struct TestPool;
397
398 impl DatabasePool for TestPool {
399 type Connection = TestConnection;
400 type Config = TestConfig;
401 type Error = TestResourceError;
402
403 fn new_resource(
404 config: &Self::Config,
405 _stale: Arc<AtomicBool>,
406 _timeout: Duration,
407 ) -> Result<Self::Connection, Error<Self::Error>> {
408 if config.fail_new_resource {
409 Err(Error::Resource(TestResourceError))
410 } else {
411 Ok(TestConnection)
412 }
413 }
414 }
415
416 fn test_config(max_size: usize, fail_new_resource: bool) -> TestConfig {
417 TestConfig {
418 max_size,
419 default_timeout: Duration::from_millis(10),
420 fail_new_resource,
421 }
422 }
423
424 fn db_connections_active() -> f64 {
425 for family in METRICS.registry().gather() {
426 if family.name() != "cdk_db_connections_active" {
427 continue;
428 }
429
430 return family
431 .get_metric()
432 .first()
433 .expect("active connections metric should exist")
434 .get_gauge()
435 .value();
436 }
437
438 panic!("active connections metric should be registered");
439 }
440
441 #[tokio::test(flavor = "current_thread")]
442 async fn active_connections_gauge_tracks_current_checkout_and_drop_counts() {
443 let _lock = crate::metrics_test_lock::lock().await;
444 METRICS.set_db_connections_active(0);
445
446 let pool = Pool::<TestPool>::new(test_config(2, false));
447
448 let first = pool
449 .get()
450 .await
451 .expect("first resource should be checked out");
452 assert_eq!(db_connections_active(), 1.0);
453
454 let second = pool
455 .get()
456 .await
457 .expect("second resource should be checked out");
458 assert_eq!(db_connections_active(), 2.0);
459
460 drop(first);
461 assert_eq!(db_connections_active(), 1.0);
462
463 drop(second);
464 assert_eq!(db_connections_active(), 0.0);
465 assert_eq!(pool.semaphore.available_permits(), pool.max_size);
466 }
467
468 #[tokio::test(flavor = "current_thread")]
469 async fn active_connections_gauge_is_restored_when_resource_creation_fails() {
470 let _lock = crate::metrics_test_lock::lock().await;
471 METRICS.set_db_connections_active(0);
472
473 let pool = Pool::<TestPool>::new(test_config(1, true));
474 let result = pool.get().await;
475
476 assert!(matches!(result, Err(Error::Resource(_))));
477 assert_eq!(db_connections_active(), 0.0);
478 assert_eq!(pool.semaphore.available_permits(), pool.max_size);
479 }
480
481 #[tokio::test(flavor = "current_thread")]
482 async fn active_connections_gauge_aggregates_multiple_pools() {
483 let _lock = crate::metrics_test_lock::lock().await;
484 METRICS.set_db_connections_active(0);
485
486 let regular_pool = Pool::<TestPool>::new(test_config(1, false));
487 let second_pool = Pool::<TestPool>::new(test_config(1, false));
488 let regular = regular_pool.get().await.expect("regular resource");
489 let second = second_pool.get().await.expect("second resource");
490
491 assert_eq!(db_connections_active(), 2.0);
492
493 drop(regular);
494 drop(second);
495 assert_eq!(db_connections_active(), 0.0);
496 }
497}