From 3ff469c8a19103997614716681c1925b44d487bc Mon Sep 17 00:00:00 2001 From: Untone Date: Mon, 22 Sep 2025 01:23:16 +0300 Subject: [PATCH] connection-pool-fix --- CHANGELOG.md | 1 + src/app_state.rs | 311 ++++++++++++++++++++++++++++++--------- src/auth.rs | 40 +++++ src/handlers/upload.rs | 37 +++-- src/handlers/user.rs | 9 +- src/lib.rs | 7 +- tests/redis_pool_test.rs | 218 +++++++++++++++++++++++++++ 7 files changed, 528 insertions(+), 95 deletions(-) create mode 100644 tests/redis_pool_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 681dd8f..387723e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - **Упрощение**: Заменена `extract_and_validate_token` на `authenticate_request` #### 🏗️ Архитектурные улучшения +- Используем redis connection pool - **Библиотечная цель**: Добавлена `lib.rs` для тестирования модулей - **Модульность**: Четкое разделение ответственности между модулями - **Единообразие**: Все handlers теперь используют одинаковую логику аутентификации diff --git a/src/app_state.rs b/src/app_state.rs index b73182e..e214ff2 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -6,11 +6,139 @@ use aws_config::BehaviorVersion; use aws_sdk_s3::{Client as S3Client, config::Credentials}; use log::warn; use redis::{AsyncCommands, Client as RedisClient, aio::MultiplexedConnection}; -use std::{env, time::Duration}; +use std::{env, sync::Arc, time::Duration}; +use tokio::sync::Mutex; + +/// Redis Connection Pool +#[derive(Clone)] +pub struct RedisConnectionPool { + client: RedisClient, + max_connections: usize, + connections: Arc>>, + timeout: Duration, +} + +impl RedisConnectionPool { + /// Создает новый connection pool + pub async fn new( + redis_url: String, + max_connections: usize, + timeout: Duration, + ) -> Result { + let client = RedisClient::open(redis_url)?; + let connections = Arc::new(Mutex::new(Vec::new())); + + let pool = Self { + client, + max_connections, + connections, + timeout, + }; + + // Предварительно создаем несколько соединений + pool.warm_up_connections(3).await?; + + Ok(pool) + } + + /// Предварительно создает соединения для пула + async fn warm_up_connections(&self, count: usize) -> Result<(), redis::RedisError> { + let mut connections = self.connections.lock().await; + + for _ in 0..count.min(self.max_connections) { + match tokio::time::timeout(self.timeout, self.client.get_multiplexed_async_connection()) + .await + { + Ok(Ok(conn)) => { + connections.push(conn); + } + Ok(Err(e)) => { + warn!("Failed to create Redis connection during warm-up: {}", e); + return Err(e); + } + Err(_) => { + warn!("Timeout creating Redis connection during warm-up"); + return Err(redis::RedisError::from(( + redis::ErrorKind::IoError, + "Connection timeout", + ))); + } + } + } + + warn!( + "✅ Redis connection pool warmed up with {} connections", + connections.len() + ); + Ok(()) + } + + /// Получает соединение из пула + pub async fn get_connection(&self) -> Result { + let mut connections = self.connections.lock().await; + + // Пытаемся взять существующее соединение + if let Some(conn) = connections.pop() { + return Ok(conn); + } + + // Если соединений нет, создаем новое + drop(connections); // Освобождаем мьютекс + + match tokio::time::timeout(self.timeout, self.client.get_multiplexed_async_connection()) + .await + { + Ok(Ok(conn)) => Ok(conn), + Ok(Err(e)) => { + warn!("Failed to create new Redis connection: {}", e); + Err(e) + } + Err(_) => { + warn!("Timeout creating new Redis connection"); + Err(redis::RedisError::from(( + redis::ErrorKind::IoError, + "Connection timeout", + ))) + } + } + } + + /// Возвращает соединение обратно в пул + pub async fn return_connection(&self, conn: MultiplexedConnection) { + let mut connections = self.connections.lock().await; + + if connections.len() < self.max_connections { + connections.push(conn); + } + // Если пул полный, соединение просто закрывается + } + + /// Проверяет здоровье пула + pub async fn health_check(&self) -> bool { + match self.get_connection().await { + Ok(mut conn) => { + match tokio::time::timeout(Duration::from_secs(2), conn.ping::()).await { + Ok(Ok(_)) => { + self.return_connection(conn).await; + true + } + _ => false, + } + } + Err(_) => false, + } + } + + /// Получает статистику пула + pub async fn get_stats(&self) -> (usize, usize) { + let connections = self.connections.lock().await; + (connections.len(), self.max_connections) + } +} #[derive(Clone)] pub struct AppState { - pub redis: Option, + pub redis_pool: Option, pub storj_client: S3Client, pub aws_client: S3Client, pub bucket: String, @@ -27,6 +155,43 @@ impl AppState { Self::new_with_config(security_config).await } + /// Получает соединение из Redis connection pool + pub async fn get_redis_connection(&self) -> Result { + if let Some(ref pool) = self.redis_pool { + pool.get_connection().await + } else { + Err(redis::RedisError::from(( + redis::ErrorKind::IoError, + "Redis pool not available", + ))) + } + } + + /// Возвращает соединение обратно в пул + pub async fn return_redis_connection(&self, conn: MultiplexedConnection) { + if let Some(ref pool) = self.redis_pool { + pool.return_connection(conn).await; + } + } + + /// Проверяет здоровье Redis connection pool + pub async fn redis_health_check(&self) -> bool { + if let Some(ref pool) = self.redis_pool { + pool.health_check().await + } else { + false + } + } + + /// Получает статистику Redis connection pool + pub async fn redis_pool_stats(&self) -> Option<(usize, usize)> { + if let Some(ref pool) = self.redis_pool { + Some(pool.get_stats().await) + } else { + None + } + } + /// Инициализация с кастомной конфигурацией безопасности. pub async fn new_with_config(security_config: SecurityConfig) -> Self { log::warn!("🚀 Starting AppState initialization..."); @@ -89,64 +254,33 @@ impl AppState { security_config: SecurityConfig, redis_url: String, ) -> Self { - let redis_client = match RedisClient::open(redis_url) { - Ok(client) => { - log::warn!("✅ Redis client created successfully"); - client - } - Err(e) => { - log::error!("❌ Failed to create Redis client: {}", e); - panic!("Redis client creation failed: {}", e); - } - }; - // Устанавливаем таймаут для Redis операций с graceful fallback log::warn!( "🔄 Attempting Redis connection with timeout: {}s", security_config.request_timeout_seconds ); - let redis_connection = match tokio::time::timeout( + // Создаем Redis connection pool + let redis_pool = match RedisConnectionPool::new( + redis_url.clone(), + 20, // max_connections согласно руководству Duration::from_secs(security_config.request_timeout_seconds), - redis_client.get_multiplexed_async_connection(), ) .await { - Ok(Ok(mut conn)) => { - log::warn!("✅ Redis connection established"); - - // Тестируем подключение простой командой - match tokio::time::timeout(Duration::from_secs(2), conn.ping::()).await { - Ok(Ok(result)) => { - log::warn!("✅ Redis PING successful: {}", result); - Some(conn) - } - Ok(Err(e)) => { - log::warn!("⚠️ Redis PING failed: {}", e); - None - } - Err(_) => { - log::warn!("⚠️ Redis PING timeout"); - None - } - } + Ok(pool) => { + log::warn!("✅ Redis connection pool created successfully"); + Some(pool) } - Ok(Err(e)) => { - log::warn!("⚠️ Redis connection failed: {}", e); - log::warn!(" Error type: {:?}", e.kind()); - log::warn!("⚠️ Running in fallback mode without Redis (quotas disabled)"); - None - } - Err(_) => { - log::warn!( - "⚠️ Redis connection timeout after {} seconds", - security_config.request_timeout_seconds - ); + Err(e) => { + log::warn!("⚠️ Redis connection pool creation failed: {}", e); log::warn!("⚠️ Running in fallback mode without Redis (quotas disabled)"); None } }; + // Одиночное соединение больше не нужно - используем только connection pool + // Получаем конфигурацию для S3 (Storj) let s3_access_key = env::var("STORJ_ACCESS_KEY").expect("STORJ_ACCESS_KEY must be set"); let s3_secret_key = env::var("STORJ_SECRET_KEY").expect("STORJ_SECRET_KEY must be set"); @@ -209,7 +343,7 @@ impl AppState { let aws_client = S3Client::new(&aws_config); let app_state = AppState { - redis: redis_connection, + redis_pool, storj_client, aws_client, bucket, @@ -226,10 +360,13 @@ impl AppState { pub async fn cache_filelist(&self) { warn!("caching AWS filelist..."); - // Проверяем доступность Redis - let Some(mut redis) = self.redis.clone() else { - warn!("⚠️ Redis not available, skipping filelist caching"); - return; + // Получаем соединение из пула + let mut redis = match self.get_redis_connection().await { + Ok(conn) => conn, + Err(_) => { + warn!("⚠️ Redis pool not available, skipping filelist caching"); + return; + } }; // Запрашиваем список файлов из Storj S3 @@ -249,31 +386,43 @@ impl AppState { } warn!("cached {} files", filelist.len()); + + // Возвращаем соединение в пул + self.return_redis_connection(redis).await; } /// Получает путь из ключа (имени файла) в Redis с таймаутом. pub async fn get_path(&self, filename: &str) -> Result, actix_web::Error> { - let Some(mut redis) = self.redis.clone() else { - warn!("⚠️ Redis not available, returning None for path lookup"); - return Ok(None); + let mut redis = match self.get_redis_connection().await { + Ok(conn) => conn, + Err(_) => { + warn!("⚠️ Redis pool not available, returning None for path lookup"); + return Ok(None); + } }; - let new_path: Option = + let result: Option = tokio::time::timeout(self.request_timeout, redis.hget(PATH_MAPPING_KEY, filename)) .await .map_err(|_| ErrorInternalServerError("Redis operation timeout"))? .map_err(|_| ErrorInternalServerError("Failed to get path mapping from Redis"))?; - Ok(new_path) + // Возвращаем соединение в пул + self.return_redis_connection(redis).await; + + Ok(result) } pub async fn set_path(&self, filename: &str, filepath: &str) { - let Some(mut redis) = self.redis.clone() else { - warn!( - "⚠️ Redis not available, skipping path caching for {}", - filename - ); - return; + let mut redis = match self.get_redis_connection().await { + Ok(conn) => conn, + Err(_) => { + warn!( + "⚠️ Redis pool not available, skipping path caching for {}", + filename + ); + return; + } }; if let Err(e) = tokio::time::timeout( @@ -284,16 +433,22 @@ impl AppState { { warn!("⚠️ Redis operation failed for {}: {}", filename, e); } + + // Возвращаем соединение в пул + self.return_redis_connection(redis).await; } /// создает или получает текущее значение квоты пользователя с таймаутом pub async fn get_or_create_quota(&self, user_id: &str) -> Result { - let Some(mut redis) = self.redis.clone() else { - warn!( - "⚠️ Redis not available, returning default quota for user {}", - user_id - ); - return Ok(0); // Возвращаем 0 как fallback + let mut redis = match self.get_redis_connection().await { + Ok(conn) => conn, + Err(_) => { + warn!( + "⚠️ Redis pool not available, returning default quota for user {}", + user_id + ); + return Ok(0); // Возвращаем 0 как fallback + } }; let quota_key = format!("quota:{}", user_id); @@ -313,8 +468,12 @@ impl AppState { .map_err(|_| ErrorInternalServerError("Redis timeout setting user quota"))? .map_err(|_| ErrorInternalServerError("Failed to set initial user quota in Redis"))?; + // Возвращаем соединение в пул + self.return_redis_connection(redis).await; Ok(0) // Возвращаем 0 как начальную квоту } else { + // Возвращаем соединение в пул + self.return_redis_connection(redis).await; Ok(quota) } } @@ -325,12 +484,15 @@ impl AppState { user_id: &str, bytes: u64, ) -> Result { - let Some(mut redis) = self.redis.clone() else { - warn!( - "⚠️ Redis not available, skipping quota increment for user {}", - user_id - ); - return Ok(0); // Возвращаем 0 как fallback + let mut redis = match self.get_redis_connection().await { + Ok(conn) => conn, + Err(_) => { + warn!( + "⚠️ Redis pool not available, skipping quota increment for user {}", + user_id + ); + return Ok(0); // Возвращаем 0 как fallback + } }; let quota_key = format!("quota:{}", user_id); @@ -354,6 +516,9 @@ impl AppState { .await .map_err(|_| ErrorInternalServerError("Redis timeout setting initial user quota"))? .map_err(|_| ErrorInternalServerError("Failed to set initial user quota in Redis"))?; + + // Возвращаем соединение в пул + self.return_redis_connection(redis).await; return Ok(bytes); } @@ -366,6 +531,8 @@ impl AppState { .map_err(|_| ErrorInternalServerError("Redis timeout incrementing user quota"))? .map_err(|_| ErrorInternalServerError("Failed to increment user quota in Redis"))?; + // Возвращаем соединение в пул + self.return_redis_connection(redis).await; Ok(new_quota) } diff --git a/src/auth.rs b/src/auth.rs index 70c8b8a..22929db 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -242,6 +242,46 @@ pub async fn authenticate_request( }) } +/// Универсальная функция аутентификации с использованием connection pool +/// Автоматически получает и возвращает соединения из пула +pub async fn authenticate_request_with_pool( + req: &actix_web::HttpRequest, + app_state: &crate::app_state::AppState, +) -> Result { + // Извлекаем токен из запроса + let token = extract_token_from_request(req).ok_or_else(|| { + warn!("No authorization token provided"); + actix_web::error::ErrorUnauthorized("Authorization token required") + })?; + + // Получаем соединение из пула + match app_state.get_redis_connection().await { + Ok(mut conn) => { + // Валидируем токен с Redis соединением + let result = + secure_token_validation(&token, Some(&mut conn), app_state.request_timeout).await; + + // Возвращаем соединение в пул + app_state.return_redis_connection(conn).await; + + result.map_err(|e| { + warn!("Token validation failed: {}", e); + actix_web::error::ErrorUnauthorized("Invalid or expired token") + }) + } + Err(_) => { + // Fallback на JWT-only валидацию если Redis недоступен + warn!("Redis pool unavailable, falling back to JWT-only validation"); + secure_token_validation(&token, None, app_state.request_timeout) + .await + .map_err(|e| { + warn!("Token validation failed: {}", e); + actix_web::error::ErrorUnauthorized("Invalid or expired token") + }) + } + } +} + /// Сохраняет имя файла в Redis для пользователя pub async fn user_added_file( redis: Option<&mut MultiplexedConnection>, diff --git a/src/handlers/upload.rs b/src/handlers/upload.rs index 9445e2e..62823bc 100644 --- a/src/handlers/upload.rs +++ b/src/handlers/upload.rs @@ -3,7 +3,7 @@ use actix_web::{HttpRequest, HttpResponse, Result, web}; use log::{error, info, warn}; use crate::app_state::AppState; -use crate::auth::{authenticate_request, user_added_file}; +use crate::auth::{authenticate_request_with_pool, user_added_file}; use crate::handlers::MAX_USER_QUOTA_BYTES; use crate::lookup::store_file_info; use crate::s3_utils::{self, generate_key_with_extension, upload_to_s3}; @@ -20,11 +20,8 @@ pub async fn upload_handler( mut payload: Multipart, state: web::Data, ) -> Result { - // Получаем Redis соединение для проверки сессий - let mut redis_conn = state.redis.clone(); - - // Универсальная аутентификация с извлечением токена и валидацией - let author = authenticate_request(&req, redis_conn.as_mut(), state.request_timeout).await?; + // Универсальная аутентификация с connection pool + let author = authenticate_request_with_pool(&req, &state).await?; let user_id = &author.user_id; info!( @@ -174,16 +171,26 @@ pub async fn upload_handler( )); } - // Сохраняем информацию о файле в Redis - if let Err(e) = store_file_info(redis_conn.as_mut(), &filename, &content_type).await - { - error!("Failed to store file info in Redis: {}", e); - // Не прерываем процесс, файл уже загружен в S3 - } + // Получаем соединение из пула для Redis операций + if let Ok(mut redis_conn) = state.get_redis_connection().await { + // Сохраняем информацию о файле в Redis + if let Err(e) = + store_file_info(Some(&mut redis_conn), &filename, &content_type).await + { + error!("Failed to store file info in Redis: {}", e); + // Не прерываем процесс, файл уже загружен в S3 + } - if let Err(e) = user_added_file(redis_conn.as_mut(), user_id, &filename).await { - error!("Failed to record user file association: {}", e); - // Не прерываем процесс + if let Err(e) = user_added_file(Some(&mut redis_conn), user_id, &filename).await + { + error!("Failed to record user file association: {}", e); + // Не прерываем процесс + } + + // Возвращаем соединение в пул + state.return_redis_connection(redis_conn).await; + } else { + warn!("Redis pool unavailable, skipping file metadata storage"); } // Сохраняем маппинг пути diff --git a/src/handlers/user.rs b/src/handlers/user.rs index a967eac..2df310f 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -3,7 +3,7 @@ use log::{error, info, warn}; use serde::Serialize; use crate::app_state::AppState; -use crate::auth::{Author, authenticate_request}; +use crate::auth::{Author, authenticate_request_with_pool}; #[derive(Serialize)] pub struct UserWithQuotaResponse { @@ -26,11 +26,8 @@ pub async fn get_current_user_handler( ) -> Result { info!("Getting user info for valid token"); - // Получаем Redis соединение для проверки сессий - let mut redis_conn = state.redis.clone(); - - // Универсальная аутентификация с извлечением токена и валидацией - let user = match authenticate_request(&req, redis_conn.as_mut(), state.request_timeout).await { + // Универсальная аутентификация с connection pool + let user = match authenticate_request_with_pool(&req, &state).await { Ok(user) => { info!( "Successfully retrieved user info: user_id={}, username={:?}", diff --git a/src/lib.rs b/src/lib.rs index 2f24ec8..70290f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,5 +10,8 @@ pub mod security; pub mod thumbnail; // Реэкспортируем основные типы для удобства -pub use app_state::AppState; -pub use auth::{Author, authenticate_request, extract_token_from_request, secure_token_validation}; +pub use app_state::{AppState, RedisConnectionPool}; +pub use auth::{ + Author, authenticate_request, authenticate_request_with_pool, extract_token_from_request, + secure_token_validation, +}; diff --git a/tests/redis_pool_test.rs b/tests/redis_pool_test.rs new file mode 100644 index 0000000..ab80013 --- /dev/null +++ b/tests/redis_pool_test.rs @@ -0,0 +1,218 @@ +use quoter::{AppState, RedisConnectionPool, authenticate_request_with_pool}; +use std::time::Duration; +use tokio::time::sleep; + +/// Тест создания Redis connection pool +#[tokio::test] +async fn test_redis_connection_pool_creation() { + // Используем невалидный URL для тестирования обработки ошибок + let invalid_url = "redis://invalid-host:6379"; + + let result = RedisConnectionPool::new(invalid_url.to_string(), 5, Duration::from_secs(1)).await; + + // Должен вернуть ошибку для невалидного URL + assert!(result.is_err()); +} + +/// Тест статистики connection pool +#[tokio::test] +async fn test_redis_pool_stats() { + // Создаем мок пула (в реальном тесте нужен валидный Redis) + // Этот тест демонстрирует API + + // В реальном окружении с Redis: + // let pool = RedisConnectionPool::new( + // "redis://localhost:6379".to_string(), + // 10, + // Duration::from_secs(5) + // ).await.unwrap(); + // + // let (available, max) = pool.get_stats().await; + // assert_eq!(max, 10); + // assert!(available <= max); + + // Для CI/CD без Redis просто проверяем, что код компилируется + assert!(true); +} + +/// Тест health check connection pool +#[tokio::test] +async fn test_redis_pool_health_check() { + // Тест с невалидным URL должен вернуть false + if let Ok(pool) = RedisConnectionPool::new( + "redis://invalid-host:6379".to_string(), + 5, + Duration::from_millis(100), // Короткий таймаут + ) + .await + { + let health = pool.health_check().await; + assert!(!health); // Должен быть false для невалидного хоста + } +} + +/// Тест получения соединения из пула +#[tokio::test] +async fn test_redis_pool_get_connection() { + // Тест с невалидным URL для проверки обработки ошибок + if let Ok(pool) = RedisConnectionPool::new( + "redis://invalid-host:6379".to_string(), + 5, + Duration::from_millis(100), + ) + .await + { + let result = pool.get_connection().await; + // Должен вернуть ошибку для невалидного хоста + assert!(result.is_err()); + } +} + +/// Тест возврата соединения в пул +#[tokio::test] +async fn test_redis_pool_return_connection() { + // Демонстрирует API для возврата соединений + // В реальном тесте с валидным Redis: + // + // let pool = RedisConnectionPool::new(...).await.unwrap(); + // let conn = pool.get_connection().await.unwrap(); + // pool.return_connection(conn).await; + // + // let (available_after, _) = pool.get_stats().await; + // assert_eq!(available_after, available_before + 1); + + assert!(true); // Проверяем компиляцию +} + +/// Тест производительности connection pool +#[tokio::test] +async fn test_redis_pool_performance() { + use std::time::Instant; + + // Тест создания пула (без реального Redis) + let start = Instant::now(); + + for _ in 0..100 { + let _result = RedisConnectionPool::new( + "redis://invalid-host:6379".to_string(), + 5, + Duration::from_millis(1), // Очень короткий таймаут + ) + .await; + // Игнорируем результат, так как Redis недоступен + } + + let duration = start.elapsed(); + println!("100 pool creation attempts took: {:?}", duration); + + // Проверяем, что операции выполняются быстро + assert!(duration < Duration::from_secs(10)); +} + +/// Тест concurrent доступа к пулу +#[tokio::test] +async fn test_redis_pool_concurrent_access() { + // Демонстрирует concurrent использование пула + let tasks = (0..10).map(|i| { + tokio::spawn(async move { + // В реальном тесте здесь был бы доступ к пулу + sleep(Duration::from_millis(i * 10)).await; + format!("Task {} completed", i) + }) + }); + + let results: Vec<_> = futures::future::join_all(tasks).await; + + // Проверяем, что все задачи завершились успешно + for (i, result) in results.iter().enumerate() { + assert!(result.is_ok()); + assert_eq!(result.as_ref().unwrap(), &format!("Task {} completed", i)); + } +} + +/// Тест AppState с Redis connection pool +#[tokio::test] +async fn test_app_state_redis_pool_methods() { + // Тестируем методы AppState для работы с пулом + // В реальном окружении нужен валидный Redis + + // Создаем AppState без Redis (для тестирования fallback) + use quoter::security::SecurityConfig; + + // Этот тест проверяет, что методы существуют и компилируются + // В реальном тесте с Redis: + // let app_state = AppState::new().await; + // let health = app_state.redis_health_check().await; + // let stats = app_state.redis_pool_stats().await; + + assert!(true); // Проверяем компиляцию +} + +/// Тест authenticate_request_with_pool +#[tokio::test] +async fn test_authenticate_request_with_pool() { + use actix_web::test; + use quoter::security::SecurityConfig; + + // Создаем тестовый запрос + let req = test::TestRequest::default() + .insert_header(("authorization", "Bearer invalid-token")) + .to_http_request(); + + // В реальном тесте здесь был бы валидный AppState с Redis + // let app_state = AppState::new().await; + // let result = authenticate_request_with_pool(&req, &app_state).await; + // assert!(result.is_err()); // Невалидный токен должен быть отклонен + + // Для CI/CD проверяем, что функция существует + assert!(true); +} + +/// Тест graceful fallback при недоступности Redis +#[tokio::test] +async fn test_redis_fallback_behavior() { + // Тестируем поведение при недоступности Redis + // Система должна работать в JWT-only режиме + + // В реальном тесте: + // 1. Создаем AppState с недоступным Redis + // 2. Проверяем, что аутентификация работает через JWT + // 3. Проверяем, что операции с квотами возвращают fallback значения + + assert!(true); // Проверяем компиляцию +} + +/// Интеграционный тест Redis connection pool +#[tokio::test] +async fn test_redis_pool_integration() { + // Полный интеграционный тест (требует реального Redis) + + // Проверяем переменную окружения для интеграционных тестов + if std::env::var("REDIS_INTEGRATION_TEST").is_ok() { + // Запускаем интеграционные тесты только если установлена переменная + let redis_url = + std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string()); + + if let Ok(pool) = RedisConnectionPool::new(redis_url, 10, Duration::from_secs(5)).await { + // Тестируем получение соединения + let conn_result = pool.get_connection().await; + assert!(conn_result.is_ok()); + + if let Ok(conn) = conn_result { + // Возвращаем соединение в пул + pool.return_connection(conn).await; + } + + // Тестируем health check + let health = pool.health_check().await; + assert!(health); + + // Тестируем статистику + let (available, max) = pool.get_stats().await; + assert_eq!(max, 10); + assert!(available <= max); + } + } else { + println!("Skipping Redis integration test (set REDIS_INTEGRATION_TEST=1 to enable)"); + } +}