//! Comprehensive connection pool edge case tests //! //! These tests cover connection pooling behavior including: //! - Pool exhaustion scenarios //! - Connection timeouts //! - Connection leaks and recovery //! - Concurrent access patterns //! - Connection lifecycle (max lifetime, idle timeout) //! - Failure scenarios and recovery //! - Transaction rollback and cleanup //! //! Requires a running PostgreSQL database: //! ```bash //! docker-compose up -d postgres //! ``` use config::database::PoolConfig; use database::{DatabaseError, DatabasePool}; use futures::stream::StreamExt; use sqlx::Acquire; use std::sync::Arc; use std::time::Duration; use tokio::time::{sleep, timeout}; /// Helper to create a test pool configuration fn create_test_pool_config(max_connections: u32, min_connections: u32) -> PoolConfig { PoolConfig { max_connections, min_connections, acquire_timeout_secs: 5, idle_timeout_secs: 30, max_lifetime_secs: 60, test_before_acquire: true, database_url: "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt" .to_string(), health_check_enabled: false, // Disable for deterministic testing health_check_interval_secs: 60, } } /// Helper to create a pool for testing async fn create_test_pool( max_connections: u32, min_connections: u32, ) -> Result { let config = create_test_pool_config(max_connections, min_connections); DatabasePool::new(config).await } #[cfg(test)] mod pool_exhaustion_tests { use super::*; #[tokio::test] async fn test_pool_exhaustion_with_small_pool() { let pool = create_test_pool(2, 1).await.expect("Pool creation failed"); // Acquire all available connections let conn1 = pool.acquire().await.expect("First connection failed"); let conn2 = pool.acquire().await.expect("Second connection failed"); // Next request should timeout let result = timeout(Duration::from_millis(100), pool.acquire()).await; assert!(result.is_err(), "Should timeout when pool is exhausted"); // Return one connection to pool drop(conn1); // Should now succeed let conn3 = pool.acquire().await; assert!(conn3.is_ok(), "Should succeed after connection returned"); drop(conn2); drop(conn3); pool.close().await; } #[tokio::test] async fn test_pool_exhaustion_recovery() { let pool = create_test_pool(3, 1).await.expect("Pool creation failed"); // Take all connections let tasks = futures::stream::FuturesUnordered::new(); for _ in 0..3 { tasks.push(pool.acquire()); } let conns: Vec<_> = tasks.collect::>().await; assert_eq!(conns.len(), 3); assert!(conns.iter().all(|c| c.is_ok())); // Pool should be exhausted let result = timeout(Duration::from_millis(100), pool.acquire()).await; assert!(result.is_err()); // Release all connections drop(conns); // Pool should recover immediately for _ in 0..3 { let conn = pool.acquire().await; assert!( conn.is_ok(), "Pool should recover after all connections returned" ); } pool.close().await; } #[tokio::test] async fn test_minimum_pool_size_single_connection() { // Test with minimum possible pool size let pool = create_test_pool(1, 1).await.expect("Pool creation failed"); let conn1 = pool.acquire().await.expect("Should get connection"); // Second request should timeout let result = timeout(Duration::from_millis(100), pool.acquire()).await; assert!( result.is_err(), "Should timeout with single connection pool" ); drop(conn1); // Should succeed after release let conn2 = pool.acquire().await; assert!(conn2.is_ok()); drop(conn2); pool.close().await; } #[tokio::test] async fn test_large_pool_no_exhaustion() { // Test with large pool size let pool = create_test_pool(100, 10) .await .expect("Pool creation failed"); // Take many connections without exhaustion let mut conns = Vec::new(); for _ in 0..50 { let conn = pool.acquire().await.expect("Should not exhaust large pool"); conns.push(conn); } // Should still have capacity let extra = pool.acquire().await; assert!(extra.is_ok(), "Large pool should still have capacity"); drop(conns); drop(extra); pool.close().await; } } #[cfg(test)] mod concurrent_access_tests { use super::*; #[tokio::test] async fn test_concurrent_connection_requests() { let pool = Arc::new(create_test_pool(10, 2).await.expect("Pool creation failed")); let mut handles = vec![]; // Spawn 50 concurrent tasks requesting connections for i in 0..50 { let pool_clone = pool.clone(); let handle = tokio::spawn(async move { let _conn = pool_clone .acquire() .await .expect(&format!("Task {} failed to acquire connection", i)); // Hold connection briefly sleep(Duration::from_millis(10)).await; }); handles.push(handle); } // All tasks should complete successfully for handle in handles { handle.await.expect("Task should complete"); } pool.close().await; } #[tokio::test] async fn test_high_concurrency_stress() { let pool = Arc::new(create_test_pool(20, 5).await.expect("Pool creation failed")); let mut handles = vec![]; // Spawn 200 tasks with high contention for i in 0..200 { let pool_clone = pool.clone(); let handle = tokio::spawn(async move { let _conn = pool_clone .acquire() .await .expect(&format!("Task {} failed to acquire", i)); // Very brief hold time to create high turnover sleep(Duration::from_millis(1)).await; }); handles.push(handle); } for handle in handles { handle.await.expect("High concurrency task should complete"); } pool.close().await; } #[tokio::test] async fn test_concurrent_with_timeouts() { let pool = Arc::new(create_test_pool(5, 1).await.expect("Pool creation failed")); let mut handles = vec![]; // Spawn more tasks than pool capacity for i in 0..15 { let pool_clone = pool.clone(); let handle = tokio::spawn(async move { // Some tasks will timeout, others succeed let result = timeout(Duration::from_millis(500), pool_clone.acquire()).await; if result.is_ok() && result.unwrap().is_ok() { sleep(Duration::from_millis(100)).await; } // Don't panic on timeout - it's expected i }); handles.push(handle); } // Count successful vs timed out let mut succeeded = 0; for handle in handles { let _ = handle.await.expect("Task should complete"); succeeded += 1; } assert_eq!(succeeded, 15, "All tasks should complete"); pool.close().await; } #[tokio::test] async fn test_sequential_vs_concurrent_performance() { let pool = create_test_pool(10, 2).await.expect("Pool creation failed"); // Sequential acquisitions let sequential_start = std::time::Instant::now(); for _ in 0..20 { let _conn = pool.acquire().await.expect("Sequential acquire failed"); sleep(Duration::from_millis(5)).await; } let sequential_duration = sequential_start.elapsed(); // Reset pool stats pool.reset_stats().await; // Concurrent acquisitions let pool = Arc::new(pool); let concurrent_start = std::time::Instant::now(); let handles: Vec<_> = (0..20) .map(|_| { let pool_clone = pool.clone(); tokio::spawn(async move { let _conn = pool_clone .acquire() .await .expect("Concurrent acquire failed"); sleep(Duration::from_millis(5)).await; }) }) .collect(); for handle in handles { handle.await.expect("Concurrent task failed"); } let concurrent_duration = concurrent_start.elapsed(); // Concurrent should be faster (or at least not much slower) assert!( concurrent_duration < sequential_duration * 2, "Concurrent should be reasonably efficient" ); pool.close().await; } } #[cfg(test)] mod connection_timeout_tests { use super::*; #[tokio::test] async fn test_acquire_timeout_honored() { // Create pool with very short timeout let mut config = create_test_pool_config(2, 1); config.acquire_timeout_secs = 1; // 1 second timeout let pool = DatabasePool::new(config) .await .expect("Pool creation failed"); // Take all connections let _conn1 = pool.acquire().await.expect("First acquire failed"); let _conn2 = pool.acquire().await.expect("Second acquire failed"); // Next acquire should timeout within ~1 second let start = std::time::Instant::now(); let result = pool.acquire().await; let elapsed = start.elapsed(); assert!(result.is_err(), "Should timeout"); assert!( elapsed < Duration::from_secs(2), "Timeout should be honored" ); pool.close().await; } #[tokio::test] async fn test_zero_timeout_immediate_failure() { // Very short timeout for immediate failure let mut config = create_test_pool_config(1, 1); config.acquire_timeout_secs = 1; let pool = DatabasePool::new(config) .await .expect("Pool creation failed"); let _conn = pool.acquire().await.expect("First acquire failed"); // Should fail almost immediately let start = std::time::Instant::now(); let result = timeout(Duration::from_millis(100), pool.acquire()).await; let elapsed = start.elapsed(); assert!(result.is_err() || result.unwrap().is_err()); assert!(elapsed < Duration::from_millis(200), "Should fail quickly"); pool.close().await; } #[tokio::test] async fn test_long_timeout_eventually_succeeds() { let mut config = create_test_pool_config(2, 1); config.acquire_timeout_secs = 10; // Long timeout let pool = Arc::new( DatabasePool::new(config) .await .expect("Pool creation failed"), ); // Take all connections let conn1 = pool.acquire().await.expect("First acquire failed"); let conn2 = pool.acquire().await.expect("Second acquire failed"); // Spawn task that will release connection after delay tokio::spawn(async move { let _c1 = conn1; let _c2 = conn2; sleep(Duration::from_millis(500)).await; // Connections dropped here }); // Should succeed once connection is released let result = pool.acquire().await; assert!(result.is_ok(), "Should succeed with long timeout"); pool.close().await; } } #[cfg(test)] mod connection_lifecycle_tests { use super::*; #[tokio::test] async fn test_connection_max_lifetime() { let mut config = create_test_pool_config(5, 2); config.max_lifetime_secs = 2; // Very short lifetime config.idle_timeout_secs = 10; // Longer idle timeout let pool = DatabasePool::new(config) .await .expect("Pool creation failed"); // Acquire and use a connection let conn = pool.acquire().await.expect("Acquire failed"); drop(conn); // Wait for connection to exceed max lifetime sleep(Duration::from_secs(3)).await; // Acquire again - should get a fresh connection let conn2 = pool.acquire().await.expect("Should get fresh connection"); drop(conn2); pool.close().await; } #[tokio::test] async fn test_connection_idle_timeout() { let mut config = create_test_pool_config(10, 2); config.idle_timeout_secs = 2; // Short idle timeout config.max_lifetime_secs = 60; // Long max lifetime let pool = DatabasePool::new(config) .await .expect("Pool creation failed"); // Create connections let tasks = futures::stream::FuturesUnordered::new(); for _ in 0..5 { tasks.push(pool.acquire()); } let conns: Vec<_> = tasks.collect::>().await; // Release all connections drop(conns); // Wait for idle timeout sleep(Duration::from_secs(3)).await; // Pool should have cleaned up idle connections let idle_count = pool.num_idle(); assert!( idle_count >= 2, "Should maintain minimum connections: {}", idle_count ); pool.close().await; } #[tokio::test] async fn test_pool_maintains_minimum_connections() { let config = create_test_pool_config(10, 5); let pool = DatabasePool::new(config) .await .expect("Pool creation failed"); // Wait for pool to initialize sleep(Duration::from_millis(500)).await; // Check pool size is at least minimum let size = pool.size(); assert!( size >= 5, "Pool should maintain minimum connections, got {}", size ); pool.close().await; } #[tokio::test] async fn test_connection_recreation_after_expiry() { let mut config = create_test_pool_config(3, 1); config.max_lifetime_secs = 1; let pool = DatabasePool::new(config) .await .expect("Pool creation failed"); let initial_size = pool.size(); // Use connections for _ in 0..5 { let _conn = pool.acquire().await.expect("Acquire failed"); sleep(Duration::from_millis(100)).await; } // Wait for connections to expire sleep(Duration::from_secs(2)).await; // Acquire new connections - should recreate let _conn = pool.acquire().await.expect("Should recreate connection"); let final_size = pool.size(); assert!( final_size >= 1, "Pool should recreate connections: initial={}, final={}", initial_size, final_size ); pool.close().await; } } #[cfg(test)] mod failure_recovery_tests { use super::*; #[tokio::test] async fn test_invalid_connection_string() { let mut config = create_test_pool_config(5, 2); config.database_url = "postgresql://invalid:invalid@nonexistent:9999/invalid".to_string(); let result = DatabasePool::new(config).await; assert!( result.is_err(), "Should fail with invalid connection string" ); } #[tokio::test] async fn test_connection_validation_before_acquire() { let mut config = create_test_pool_config(5, 2); config.test_before_acquire = true; let pool = DatabasePool::new(config) .await .expect("Pool creation failed"); // Acquire connection with validation let mut conn = pool .acquire() .await .expect("Acquire with validation failed"); // Connection should be valid let result = sqlx::query("SELECT 1").fetch_one(&mut *conn).await; assert!(result.is_ok(), "Connection should be valid"); pool.close().await; } #[tokio::test] async fn test_pool_recovery_after_close() { let pool = create_test_pool(5, 2).await.expect("Pool creation failed"); // Close the pool pool.close().await; assert!(pool.is_closed()); // Attempts to acquire should fail let result = timeout(Duration::from_millis(100), pool.acquire()).await; assert!( result.is_err() || result.unwrap().is_err(), "Acquire should fail on closed pool" ); } #[tokio::test] async fn test_graceful_pool_shutdown() { let pool = create_test_pool(5, 2).await.expect("Pool creation failed"); // Acquire some connections let tasks = futures::stream::FuturesUnordered::new(); for _ in 0..3 { tasks.push(pool.acquire()); } let conns: Vec<_> = tasks.collect::>().await; // Close pool pool.close().await; // Existing connections should still work for conn in conns { if let Ok(mut c) = conn { let result = sqlx::query("SELECT 1").fetch_one(&mut *c).await; assert!(result.is_ok(), "Existing connection should still work"); } } } #[tokio::test] async fn test_pool_health_check() { let pool = create_test_pool(5, 2).await.expect("Pool creation failed"); let health = pool.health_check().await.expect("Health check failed"); assert!(health, "Pool should be healthy"); pool.close().await; // Health check on closed pool let health_after_close = pool.health_check().await; assert!( !health_after_close.unwrap_or(false), "Closed pool should be unhealthy" ); } } #[cfg(test)] mod transaction_tests { use super::*; #[tokio::test] async fn test_transaction_connection_returned_on_commit() { let pool = create_test_pool(3, 1).await.expect("Pool creation failed"); let initial_idle = pool.num_idle(); { let mut conn = pool.acquire().await.expect("Acquire failed"); let mut tx = conn.begin().await.expect("Begin transaction failed"); sqlx::query("SELECT 1") .execute(&mut *tx) .await .expect("Query failed"); tx.commit().await.expect("Commit failed"); } // Connection dropped here // Wait for connection to return to pool sleep(Duration::from_millis(100)).await; let final_idle = pool.num_idle(); assert!( final_idle >= initial_idle, "Connection should return to pool after commit" ); pool.close().await; } #[tokio::test] async fn test_transaction_connection_returned_on_rollback() { let pool = create_test_pool(3, 1).await.expect("Pool creation failed"); let initial_idle = pool.num_idle(); { let mut conn = pool.acquire().await.expect("Acquire failed"); let mut tx = conn.begin().await.expect("Begin transaction failed"); sqlx::query("SELECT 1") .execute(&mut *tx) .await .expect("Query failed"); tx.rollback().await.expect("Rollback failed"); } // Connection dropped here // Wait for connection to return to pool sleep(Duration::from_millis(100)).await; let final_idle = pool.num_idle(); assert!( final_idle >= initial_idle, "Connection should return to pool after rollback" ); pool.close().await; } #[tokio::test] async fn test_failed_transaction_cleanup() { let pool = create_test_pool(3, 1).await.expect("Pool creation failed"); let initial_idle = pool.num_idle(); { let mut conn = pool.acquire().await.expect("Acquire failed"); let mut tx = conn.begin().await.expect("Begin transaction failed"); // Execute invalid query to cause failure let _ = sqlx::query("SELECT * FROM nonexistent_table_xyz") .execute(&mut *tx) .await; // Transaction will be rolled back on drop } // Connection dropped here // Wait for cleanup sleep(Duration::from_millis(100)).await; // Connection should still return to pool despite failed transaction let final_idle = pool.num_idle(); assert!( final_idle >= initial_idle, "Connection should return even after failed transaction" ); pool.close().await; } } #[cfg(test)] mod pool_statistics_tests { use super::*; #[tokio::test] async fn test_pool_stats_acquisition_tracking() { let pool = create_test_pool(5, 2).await.expect("Pool creation failed"); pool.reset_stats().await; // Perform acquisitions for _ in 0..10 { let _conn = pool.acquire().await.expect("Acquire failed"); } let stats = pool.stats().await; assert_eq!( stats.total_acquisitions, 10, "Should track total acquisitions" ); pool.close().await; } #[tokio::test] async fn test_pool_stats_failed_acquisitions() { let pool = create_test_pool(2, 1).await.expect("Pool creation failed"); pool.reset_stats().await; // Take all connections let _conn1 = pool.acquire().await.expect("First acquire failed"); let _conn2 = pool.acquire().await.expect("Second acquire failed"); // Try to acquire more (will timeout and fail) for _ in 0..3 { let _ = timeout(Duration::from_millis(50), pool.acquire()).await; } let stats = pool.stats().await; assert!( stats.total_acquisitions >= 5, "Should track all acquisition attempts" ); pool.close().await; } #[tokio::test] async fn test_pool_stats_reset() { let pool = create_test_pool(5, 2).await.expect("Pool creation failed"); // Perform some operations for _ in 0..5 { let _conn = pool.acquire().await.expect("Acquire failed"); } let stats_before = pool.stats().await; assert!(stats_before.total_acquisitions > 0); // Reset stats pool.reset_stats().await; let stats_after = pool.stats().await; assert_eq!(stats_after.total_acquisitions, 0, "Stats should be reset"); pool.close().await; } #[tokio::test] async fn test_pool_size_metrics() { let pool = create_test_pool(10, 3).await.expect("Pool creation failed"); // Wait for minimum connections to be established sleep(Duration::from_millis(500)).await; let size = pool.size(); let idle = pool.num_idle(); assert!(size >= 3, "Pool size should be at least minimum: {}", size); assert!(idle <= size as usize, "Idle cannot exceed total size"); // Acquire connections let tasks = futures::stream::FuturesUnordered::new(); for _ in 0..5 { tasks.push(pool.acquire()); } let conns: Vec<_> = tasks.collect::>().await; let idle_after = pool.num_idle(); assert!( idle_after < idle, "Idle connections should decrease after acquisitions" ); drop(conns); pool.close().await; } } #[cfg(test)] mod configuration_validation_tests { use super::*; #[tokio::test] async fn test_invalid_min_max_configuration() { let mut config = create_test_pool_config(5, 10); config.min_connections = 10; // Min > Max config.max_connections = 5; let result = DatabasePool::new(config).await; assert!(result.is_err(), "Should reject min > max configuration"); } #[tokio::test] async fn test_zero_max_connections() { let mut config = create_test_pool_config(0, 0); config.max_connections = 0; let result = DatabasePool::new(config).await; assert!(result.is_err(), "Should reject zero max connections"); } #[tokio::test] async fn test_invalid_database_url() { let mut config = create_test_pool_config(5, 2); config.database_url = "not-a-valid-url".to_string(); let result = DatabasePool::new(config).await; assert!(result.is_err(), "Should reject invalid database URL"); } #[tokio::test] async fn test_min_equals_max_configuration() { let pool = create_test_pool(5, 5).await.expect("Pool creation failed"); sleep(Duration::from_millis(500)).await; let size = pool.size(); assert_eq!(size, 5, "Pool should maintain fixed size when min=max"); pool.close().await; } }