1use super::config::PoolConfig;
4use super::errors::{PoolError, PoolResult};
5use super::events::{PoolEvent, PoolEventListener};
6use sqlx::{Database, MySql, Pool, Postgres, Sqlite};
7use std::mem::ManuallyDrop;
8use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
9use std::sync::{Arc, OnceLock};
10use tokio::sync::RwLock;
11
12pub(crate) fn mask_url_password(url: &str) -> String {
19 if let Some(scheme_end) = url.find("://") {
21 let after_scheme = &url[scheme_end + 3..];
22
23 if let Some(at_pos) = after_scheme.rfind('@') {
25 let user_info = &after_scheme[..at_pos];
26
27 if let Some(colon_pos) = user_info.find(':') {
29 let scheme_and_user = &url[..scheme_end + 3 + colon_pos + 1];
30 let rest = &url[scheme_end + 3 + at_pos..];
31 return format!("{}***{}", scheme_and_user, rest);
32 }
33 }
34 }
35
36 url.to_string()
38}
39
40fn generate_connection_id() -> String {
41 uuid::Uuid::now_v7().to_string()
42}
43
44struct PoolEventHub {
45 listeners: RwLock<Vec<Arc<dyn PoolEventListener>>>,
46 listener_count: AtomicUsize,
47}
48
49impl PoolEventHub {
50 fn new() -> Self {
51 Self {
52 listeners: RwLock::new(Vec::new()),
53 listener_count: AtomicUsize::new(0),
54 }
55 }
56
57 fn has_listeners(&self) -> bool {
58 self.listener_count.load(Ordering::Acquire) > 0
59 }
60
61 async fn add_listener(&self, listener: Arc<dyn PoolEventListener>) {
62 let mut listeners = self.listeners.write().await;
63 listeners.push(listener);
64 self.listener_count
65 .store(listeners.len(), Ordering::Release);
66 }
67
68 async fn emit_event(&self, event: PoolEvent) {
69 if !self.has_listeners() {
70 return;
71 }
72
73 let listeners = self.listeners.read().await;
74 for listener in listeners.iter() {
75 listener.on_event(event.clone()).await;
76 }
77 }
78}
79
80pub struct ConnectionPool<DB: Database> {
82 pool: Pool<DB>,
83 config: PoolConfig,
84 url: String,
85 events: Arc<PoolEventHub>,
86 first_connect_fired: Arc<AtomicBool>,
87}
88
89impl ConnectionPool<Postgres> {
90 pub async fn new_postgres(url: &str, config: PoolConfig) -> PoolResult<Self> {
107 config.validate().map_err(PoolError::Config)?;
108
109 let pool = sqlx::postgres::PgPoolOptions::new()
110 .min_connections(config.min_connections)
111 .max_connections(config.max_connections)
112 .acquire_timeout(config.acquire_timeout)
113 .idle_timeout(config.idle_timeout)
114 .max_lifetime(config.max_lifetime)
115 .test_before_acquire(config.test_before_acquire)
116 .connect(url)
117 .await?;
118
119 Ok(Self {
120 pool,
121 config,
122 url: url.to_string(),
123 events: Arc::new(PoolEventHub::new()),
124 first_connect_fired: Arc::new(AtomicBool::new(false)),
125 })
126 }
127}
128
129impl ConnectionPool<MySql> {
130 pub async fn new_mysql(url: &str, config: PoolConfig) -> PoolResult<Self> {
147 config.validate().map_err(PoolError::Config)?;
148
149 let pool = sqlx::mysql::MySqlPoolOptions::new()
150 .min_connections(config.min_connections)
151 .max_connections(config.max_connections)
152 .acquire_timeout(config.acquire_timeout)
153 .idle_timeout(config.idle_timeout)
154 .max_lifetime(config.max_lifetime)
155 .test_before_acquire(config.test_before_acquire)
156 .connect(url)
157 .await?;
158
159 Ok(Self {
160 pool,
161 config,
162 url: url.to_string(),
163 events: Arc::new(PoolEventHub::new()),
164 first_connect_fired: Arc::new(AtomicBool::new(false)),
165 })
166 }
167}
168
169impl ConnectionPool<Sqlite> {
170 pub async fn new_sqlite(url: &str, config: PoolConfig) -> PoolResult<Self> {
187 config.validate().map_err(PoolError::Config)?;
188
189 let pool = sqlx::sqlite::SqlitePoolOptions::new()
190 .min_connections(config.min_connections)
191 .max_connections(config.max_connections)
192 .acquire_timeout(config.acquire_timeout)
193 .idle_timeout(config.idle_timeout)
194 .max_lifetime(config.max_lifetime)
195 .test_before_acquire(config.test_before_acquire)
196 .connect(url)
197 .await?;
198
199 Ok(Self {
200 pool,
201 config,
202 url: url.to_string(),
203 events: Arc::new(PoolEventHub::new()),
204 first_connect_fired: Arc::new(AtomicBool::new(false)),
205 })
206 }
207}
208
209impl<DB> ConnectionPool<DB>
210where
211 DB: sqlx::Database,
212{
213 pub async fn add_listener(&self, listener: Arc<dyn PoolEventListener>) {
216 self.events.add_listener(listener).await;
217 }
218
219 pub(crate) async fn emit_event(&self, event: PoolEvent) {
221 self.events.emit_event(event).await;
222 }
223 pub async fn acquire(&self) -> PoolResult<PooledConnection<DB>> {
242 let is_first = !self.first_connect_fired.swap(true, Ordering::SeqCst);
244
245 let conn = self.pool.acquire().await?;
246 let connection_id = OnceLock::new();
247
248 if self.events.has_listeners() {
249 let id = connection_id.get_or_init(generate_connection_id).clone();
250
251 if is_first {
252 self.emit_event(PoolEvent::connection_created(id.clone()))
254 .await;
255 }
256
257 self.emit_event(PoolEvent::connection_acquired(id)).await;
259 }
260
261 Ok(PooledConnection {
262 conn: ManuallyDrop::new(conn),
263 events: self.events.clone(),
264 connection_id,
265 })
266 }
267 pub fn inner(&self) -> &Pool<DB> {
270 &self.pool
271 }
272 pub fn config(&self) -> &PoolConfig {
275 &self.config
276 }
277 pub async fn close(&self) {
283 use tokio::time::{Duration, timeout};
284
285 let close_future = self.pool.close();
287 if timeout(Duration::from_secs(5), close_future).await.is_err() {
288 }
291 }
292 pub fn url(&self) -> String {
298 mask_url_password(&self.url)
299 }
300
301 #[allow(dead_code)]
307 pub(crate) fn url_raw(&self) -> &str {
308 &self.url
309 }
310}
311
312impl ConnectionPool<Postgres> {
314 pub async fn recreate(&mut self) -> PoolResult<()> {
335 self.pool.close().await;
337
338 let new_pool = sqlx::postgres::PgPoolOptions::new()
340 .min_connections(self.config.min_connections)
341 .max_connections(self.config.max_connections)
342 .acquire_timeout(self.config.acquire_timeout)
343 .idle_timeout(self.config.idle_timeout)
344 .max_lifetime(self.config.max_lifetime)
345 .test_before_acquire(self.config.test_before_acquire)
346 .connect(&self.url)
347 .await?;
348
349 self.pool = new_pool;
350 self.first_connect_fired.store(false, Ordering::SeqCst);
351
352 Ok(())
353 }
354}
355
356impl ConnectionPool<MySql> {
357 pub async fn recreate(&mut self) -> PoolResult<()> {
378 self.pool.close().await;
380
381 let new_pool = sqlx::mysql::MySqlPoolOptions::new()
383 .min_connections(self.config.min_connections)
384 .max_connections(self.config.max_connections)
385 .acquire_timeout(self.config.acquire_timeout)
386 .idle_timeout(self.config.idle_timeout)
387 .max_lifetime(self.config.max_lifetime)
388 .test_before_acquire(self.config.test_before_acquire)
389 .connect(&self.url)
390 .await?;
391
392 self.pool = new_pool;
393 self.first_connect_fired.store(false, Ordering::SeqCst);
394
395 Ok(())
396 }
397}
398
399impl ConnectionPool<Sqlite> {
400 pub async fn recreate(&mut self) -> PoolResult<()> {
420 self.pool.close().await;
422
423 let new_pool = sqlx::sqlite::SqlitePoolOptions::new()
425 .min_connections(self.config.min_connections)
426 .max_connections(self.config.max_connections)
427 .acquire_timeout(self.config.acquire_timeout)
428 .idle_timeout(self.config.idle_timeout)
429 .max_lifetime(self.config.max_lifetime)
430 .test_before_acquire(self.config.test_before_acquire)
431 .connect(&self.url)
432 .await?;
433
434 self.pool = new_pool;
435 self.first_connect_fired.store(false, Ordering::SeqCst);
436
437 Ok(())
438 }
439}
440
441pub struct PooledConnection<DB: sqlx::Database> {
443 conn: ManuallyDrop<sqlx::pool::PoolConnection<DB>>,
447 events: Arc<PoolEventHub>,
448 connection_id: OnceLock<String>,
449}
450
451impl<DB: sqlx::Database> PooledConnection<DB> {
452 pub fn inner(&mut self) -> &mut sqlx::pool::PoolConnection<DB> {
455 &mut self.conn
456 }
457 pub fn connection_id(&self) -> &str {
476 self.connection_id
477 .get_or_init(generate_connection_id)
478 .as_str()
479 }
480 pub async fn invalidate(self, reason: String) {
483 if self.events.has_listeners() {
484 let connection_id = self
485 .connection_id
486 .get_or_init(generate_connection_id)
487 .clone();
488 self.events
489 .emit_event(PoolEvent::connection_invalidated(connection_id, reason))
490 .await;
491 }
492 }
494 pub async fn soft_invalidate(&mut self) {
497 if self.events.has_listeners() {
498 let connection_id = self
499 .connection_id
500 .get_or_init(generate_connection_id)
501 .clone();
502 self.events
503 .emit_event(PoolEvent::connection_soft_invalidated(connection_id))
504 .await;
505 }
506 }
507 pub async fn reset(&mut self) {
510 if self.events.has_listeners() {
511 let connection_id = self
512 .connection_id
513 .get_or_init(generate_connection_id)
514 .clone();
515 self.events
516 .emit_event(PoolEvent::connection_reset(connection_id))
517 .await;
518 }
519 }
520}
521
522impl<DB: sqlx::Database> Drop for PooledConnection<DB> {
523 fn drop(&mut self) {
524 let conn = unsafe { ManuallyDrop::take(&mut self.conn) };
526
527 match tokio::runtime::Handle::try_current() {
528 Ok(handle) => {
529 drop(conn);
532
533 if self.events.has_listeners() {
534 let events = self.events.clone();
535 let connection_id = self
536 .connection_id
537 .get_or_init(generate_connection_id)
538 .clone();
539
540 handle.spawn(async move {
541 events
542 .emit_event(PoolEvent::connection_returned(connection_id))
543 .await;
544 });
545 }
546 }
547 Err(_) => {
548 std::mem::forget(conn);
553 }
554 }
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561 use rstest::rstest;
562 use std::sync::Mutex;
563
564 struct RecordingListener {
565 events: Arc<Mutex<Vec<&'static str>>>,
566 }
567
568 #[async_trait::async_trait]
569 impl PoolEventListener for RecordingListener {
570 async fn on_event(&self, event: PoolEvent) {
571 let name = match event {
572 PoolEvent::ConnectionAcquired { .. } => "acquired",
573 PoolEvent::ConnectionReturned { .. } => "returned",
574 PoolEvent::ConnectionCreated { .. } => "created",
575 PoolEvent::ConnectionClosed { .. } => "closed",
576 PoolEvent::ConnectionTestFailed { .. } => "test_failed",
577 PoolEvent::ConnectionInvalidated { .. } => "invalidated",
578 PoolEvent::ConnectionSoftInvalidated { .. } => "soft_invalidated",
579 PoolEvent::ConnectionReset { .. } => "reset",
580 };
581 self.events
582 .lock()
583 .expect("events mutex should not be poisoned")
584 .push(name);
585 }
586 }
587
588 #[rstest]
589 #[case(
590 "postgresql://user:secret@localhost:5432/mydb",
591 "postgresql://user:***@localhost:5432/mydb"
592 )]
593 #[case(
594 "mysql://admin:p@ssw0rd@db.example.com/app",
595 "mysql://admin:***@db.example.com/app"
596 )]
597 #[case(
598 "postgres://user:pass@host:5432/db?sslmode=require",
599 "postgres://user:***@host:5432/db?sslmode=require"
600 )]
601 fn test_mask_url_password_with_credentials(#[case] input: &str, #[case] expected: &str) {
602 let masked = mask_url_password(input);
607
608 assert_eq!(masked, expected);
610 }
611
612 #[rstest]
613 #[case("sqlite::memory:")]
614 #[case("sqlite:///path/to/db.sqlite")]
615 #[case("postgresql://user@localhost:5432/mydb")]
616 fn test_mask_url_password_without_password(#[case] input: &str) {
617 let masked = mask_url_password(input);
622
623 assert_eq!(masked, input, "URL without password should be unchanged");
625 }
626
627 #[rstest]
628 fn test_mask_url_password_empty_password() {
629 let url = "postgresql://user:@localhost:5432/mydb";
631
632 let masked = mask_url_password(url);
634
635 assert_eq!(masked, "postgresql://user:***@localhost:5432/mydb");
637 }
638
639 #[rstest]
640 fn test_mask_url_password_special_chars_in_password() {
641 let url = "postgresql://user:p%40ss%3Aw0rd@localhost:5432/mydb";
643
644 let masked = mask_url_password(url);
646
647 assert_eq!(masked, "postgresql://user:***@localhost:5432/mydb");
649 assert!(
650 !masked.contains("p%40ss"),
651 "Password should be fully masked"
652 );
653 }
654
655 #[rstest]
656 fn test_mask_url_password_preserves_non_url() {
657 let non_url = "not-a-url-just-a-string";
659
660 let masked = mask_url_password(non_url);
662
663 assert_eq!(
665 masked, non_url,
666 "Non-URL strings should pass through unchanged"
667 );
668 }
669
670 #[rstest]
671 fn test_handle_try_current_returns_err_outside_runtime() {
672 let handle = std::thread::spawn(|| {
676 let result = tokio::runtime::Handle::try_current();
677 assert!(
678 result.is_err(),
679 "Handle::try_current() should return Err outside of a tokio runtime"
680 );
681 });
682 handle.join().expect("thread should not panic");
683 }
684
685 #[rstest]
686 fn test_drop_pooled_connection_outside_runtime_does_not_panic() {
687 let rt = tokio::runtime::Runtime::new().expect("failed to create Tokio runtime");
690
691 let (pool, conn) = rt.block_on(async {
692 let config = PoolConfig::default();
693 let pool = ConnectionPool::new_sqlite("sqlite::memory:", config)
694 .await
695 .expect("failed to create ConnectionPool");
696
697 let conn = pool.acquire().await.expect("failed to acquire connection");
698
699 (pool, conn)
700 });
701
702 drop(rt);
704
705 drop(conn);
708
709 drop(pool);
711 }
712
713 #[tokio::test]
714 async fn test_pool_events_are_emitted_when_listener_registered() {
715 let events = Arc::new(Mutex::new(Vec::new()));
717 let listener = Arc::new(RecordingListener {
718 events: events.clone(),
719 });
720 let pool = ConnectionPool::new_sqlite("sqlite::memory:", PoolConfig::default())
721 .await
722 .expect("failed to create ConnectionPool");
723 pool.add_listener(listener).await;
724
725 let conn = pool.acquire().await.expect("failed to acquire connection");
727 drop(conn);
728 tokio::task::yield_now().await;
729
730 let recorded = events.lock().expect("events mutex should not be poisoned");
732 assert_eq!(
733 recorded.as_slice(),
734 ["created", "acquired", "returned"],
735 "pool listener should observe the first acquire and return events"
736 );
737 }
738}