This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
- **Упрощение**: Заменена `extract_and_validate_token` на `authenticate_request`
|
- **Упрощение**: Заменена `extract_and_validate_token` на `authenticate_request`
|
||||||
|
|
||||||
#### 🏗️ Архитектурные улучшения
|
#### 🏗️ Архитектурные улучшения
|
||||||
|
- Используем redis connection pool
|
||||||
- **Библиотечная цель**: Добавлена `lib.rs` для тестирования модулей
|
- **Библиотечная цель**: Добавлена `lib.rs` для тестирования модулей
|
||||||
- **Модульность**: Четкое разделение ответственности между модулями
|
- **Модульность**: Четкое разделение ответственности между модулями
|
||||||
- **Единообразие**: Все handlers теперь используют одинаковую логику аутентификации
|
- **Единообразие**: Все handlers теперь используют одинаковую логику аутентификации
|
||||||
|
|||||||
311
src/app_state.rs
311
src/app_state.rs
@@ -6,11 +6,139 @@ use aws_config::BehaviorVersion;
|
|||||||
use aws_sdk_s3::{Client as S3Client, config::Credentials};
|
use aws_sdk_s3::{Client as S3Client, config::Credentials};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use redis::{AsyncCommands, Client as RedisClient, aio::MultiplexedConnection};
|
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<Mutex<Vec<MultiplexedConnection>>>,
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedisConnectionPool {
|
||||||
|
/// Создает новый connection pool
|
||||||
|
pub async fn new(
|
||||||
|
redis_url: String,
|
||||||
|
max_connections: usize,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<Self, redis::RedisError> {
|
||||||
|
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<MultiplexedConnection, redis::RedisError> {
|
||||||
|
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::<String>()).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)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub redis: Option<MultiplexedConnection>,
|
pub redis_pool: Option<RedisConnectionPool>,
|
||||||
pub storj_client: S3Client,
|
pub storj_client: S3Client,
|
||||||
pub aws_client: S3Client,
|
pub aws_client: S3Client,
|
||||||
pub bucket: String,
|
pub bucket: String,
|
||||||
@@ -27,6 +155,43 @@ impl AppState {
|
|||||||
Self::new_with_config(security_config).await
|
Self::new_with_config(security_config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Получает соединение из Redis connection pool
|
||||||
|
pub async fn get_redis_connection(&self) -> Result<MultiplexedConnection, redis::RedisError> {
|
||||||
|
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 {
|
pub async fn new_with_config(security_config: SecurityConfig) -> Self {
|
||||||
log::warn!("🚀 Starting AppState initialization...");
|
log::warn!("🚀 Starting AppState initialization...");
|
||||||
@@ -89,64 +254,33 @@ impl AppState {
|
|||||||
security_config: SecurityConfig,
|
security_config: SecurityConfig,
|
||||||
redis_url: String,
|
redis_url: String,
|
||||||
) -> Self {
|
) -> 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
|
// Устанавливаем таймаут для Redis операций с graceful fallback
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"🔄 Attempting Redis connection with timeout: {}s",
|
"🔄 Attempting Redis connection with timeout: {}s",
|
||||||
security_config.request_timeout_seconds
|
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),
|
Duration::from_secs(security_config.request_timeout_seconds),
|
||||||
redis_client.get_multiplexed_async_connection(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Ok(mut conn)) => {
|
Ok(pool) => {
|
||||||
log::warn!("✅ Redis connection established");
|
log::warn!("✅ Redis connection pool created successfully");
|
||||||
|
Some(pool)
|
||||||
// Тестируем подключение простой командой
|
|
||||||
match tokio::time::timeout(Duration::from_secs(2), conn.ping::<String>()).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(Err(e)) => {
|
Err(e) => {
|
||||||
log::warn!("⚠️ Redis connection failed: {}", e);
|
log::warn!("⚠️ Redis connection pool creation 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
|
|
||||||
);
|
|
||||||
log::warn!("⚠️ Running in fallback mode without Redis (quotas disabled)");
|
log::warn!("⚠️ Running in fallback mode without Redis (quotas disabled)");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Одиночное соединение больше не нужно - используем только connection pool
|
||||||
|
|
||||||
// Получаем конфигурацию для S3 (Storj)
|
// Получаем конфигурацию для S3 (Storj)
|
||||||
let s3_access_key = env::var("STORJ_ACCESS_KEY").expect("STORJ_ACCESS_KEY must be set");
|
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");
|
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 aws_client = S3Client::new(&aws_config);
|
||||||
|
|
||||||
let app_state = AppState {
|
let app_state = AppState {
|
||||||
redis: redis_connection,
|
redis_pool,
|
||||||
storj_client,
|
storj_client,
|
||||||
aws_client,
|
aws_client,
|
||||||
bucket,
|
bucket,
|
||||||
@@ -226,10 +360,13 @@ impl AppState {
|
|||||||
pub async fn cache_filelist(&self) {
|
pub async fn cache_filelist(&self) {
|
||||||
warn!("caching AWS filelist...");
|
warn!("caching AWS filelist...");
|
||||||
|
|
||||||
// Проверяем доступность Redis
|
// Получаем соединение из пула
|
||||||
let Some(mut redis) = self.redis.clone() else {
|
let mut redis = match self.get_redis_connection().await {
|
||||||
warn!("⚠️ Redis not available, skipping filelist caching");
|
Ok(conn) => conn,
|
||||||
return;
|
Err(_) => {
|
||||||
|
warn!("⚠️ Redis pool not available, skipping filelist caching");
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Запрашиваем список файлов из Storj S3
|
// Запрашиваем список файлов из Storj S3
|
||||||
@@ -249,31 +386,43 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
warn!("cached {} files", filelist.len());
|
warn!("cached {} files", filelist.len());
|
||||||
|
|
||||||
|
// Возвращаем соединение в пул
|
||||||
|
self.return_redis_connection(redis).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Получает путь из ключа (имени файла) в Redis с таймаутом.
|
/// Получает путь из ключа (имени файла) в Redis с таймаутом.
|
||||||
pub async fn get_path(&self, filename: &str) -> Result<Option<String>, actix_web::Error> {
|
pub async fn get_path(&self, filename: &str) -> Result<Option<String>, actix_web::Error> {
|
||||||
let Some(mut redis) = self.redis.clone() else {
|
let mut redis = match self.get_redis_connection().await {
|
||||||
warn!("⚠️ Redis not available, returning None for path lookup");
|
Ok(conn) => conn,
|
||||||
return Ok(None);
|
Err(_) => {
|
||||||
|
warn!("⚠️ Redis pool not available, returning None for path lookup");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_path: Option<String> =
|
let result: Option<String> =
|
||||||
tokio::time::timeout(self.request_timeout, redis.hget(PATH_MAPPING_KEY, filename))
|
tokio::time::timeout(self.request_timeout, redis.hget(PATH_MAPPING_KEY, filename))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ErrorInternalServerError("Redis operation timeout"))?
|
.map_err(|_| ErrorInternalServerError("Redis operation timeout"))?
|
||||||
.map_err(|_| ErrorInternalServerError("Failed to get path mapping from Redis"))?;
|
.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) {
|
pub async fn set_path(&self, filename: &str, filepath: &str) {
|
||||||
let Some(mut redis) = self.redis.clone() else {
|
let mut redis = match self.get_redis_connection().await {
|
||||||
warn!(
|
Ok(conn) => conn,
|
||||||
"⚠️ Redis not available, skipping path caching for {}",
|
Err(_) => {
|
||||||
filename
|
warn!(
|
||||||
);
|
"⚠️ Redis pool not available, skipping path caching for {}",
|
||||||
return;
|
filename
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = tokio::time::timeout(
|
if let Err(e) = tokio::time::timeout(
|
||||||
@@ -284,16 +433,22 @@ impl AppState {
|
|||||||
{
|
{
|
||||||
warn!("⚠️ Redis operation failed for {}: {}", filename, e);
|
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<u64, actix_web::Error> {
|
pub async fn get_or_create_quota(&self, user_id: &str) -> Result<u64, actix_web::Error> {
|
||||||
let Some(mut redis) = self.redis.clone() else {
|
let mut redis = match self.get_redis_connection().await {
|
||||||
warn!(
|
Ok(conn) => conn,
|
||||||
"⚠️ Redis not available, returning default quota for user {}",
|
Err(_) => {
|
||||||
user_id
|
warn!(
|
||||||
);
|
"⚠️ Redis pool not available, returning default quota for user {}",
|
||||||
return Ok(0); // Возвращаем 0 как fallback
|
user_id
|
||||||
|
);
|
||||||
|
return Ok(0); // Возвращаем 0 как fallback
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let quota_key = format!("quota:{}", user_id);
|
let quota_key = format!("quota:{}", user_id);
|
||||||
|
|
||||||
@@ -313,8 +468,12 @@ impl AppState {
|
|||||||
.map_err(|_| ErrorInternalServerError("Redis timeout setting user quota"))?
|
.map_err(|_| ErrorInternalServerError("Redis timeout setting user quota"))?
|
||||||
.map_err(|_| ErrorInternalServerError("Failed to set initial user quota in Redis"))?;
|
.map_err(|_| ErrorInternalServerError("Failed to set initial user quota in Redis"))?;
|
||||||
|
|
||||||
|
// Возвращаем соединение в пул
|
||||||
|
self.return_redis_connection(redis).await;
|
||||||
Ok(0) // Возвращаем 0 как начальную квоту
|
Ok(0) // Возвращаем 0 как начальную квоту
|
||||||
} else {
|
} else {
|
||||||
|
// Возвращаем соединение в пул
|
||||||
|
self.return_redis_connection(redis).await;
|
||||||
Ok(quota)
|
Ok(quota)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,12 +484,15 @@ impl AppState {
|
|||||||
user_id: &str,
|
user_id: &str,
|
||||||
bytes: u64,
|
bytes: u64,
|
||||||
) -> Result<u64, actix_web::Error> {
|
) -> Result<u64, actix_web::Error> {
|
||||||
let Some(mut redis) = self.redis.clone() else {
|
let mut redis = match self.get_redis_connection().await {
|
||||||
warn!(
|
Ok(conn) => conn,
|
||||||
"⚠️ Redis not available, skipping quota increment for user {}",
|
Err(_) => {
|
||||||
user_id
|
warn!(
|
||||||
);
|
"⚠️ Redis pool not available, skipping quota increment for user {}",
|
||||||
return Ok(0); // Возвращаем 0 как fallback
|
user_id
|
||||||
|
);
|
||||||
|
return Ok(0); // Возвращаем 0 как fallback
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let quota_key = format!("quota:{}", user_id);
|
let quota_key = format!("quota:{}", user_id);
|
||||||
|
|
||||||
@@ -354,6 +516,9 @@ impl AppState {
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| ErrorInternalServerError("Redis timeout setting initial user quota"))?
|
.map_err(|_| ErrorInternalServerError("Redis timeout setting initial user quota"))?
|
||||||
.map_err(|_| ErrorInternalServerError("Failed to set initial user quota in Redis"))?;
|
.map_err(|_| ErrorInternalServerError("Failed to set initial user quota in Redis"))?;
|
||||||
|
|
||||||
|
// Возвращаем соединение в пул
|
||||||
|
self.return_redis_connection(redis).await;
|
||||||
return Ok(bytes);
|
return Ok(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +531,8 @@ impl AppState {
|
|||||||
.map_err(|_| ErrorInternalServerError("Redis timeout incrementing user quota"))?
|
.map_err(|_| ErrorInternalServerError("Redis timeout incrementing user quota"))?
|
||||||
.map_err(|_| ErrorInternalServerError("Failed to increment user quota in Redis"))?;
|
.map_err(|_| ErrorInternalServerError("Failed to increment user quota in Redis"))?;
|
||||||
|
|
||||||
|
// Возвращаем соединение в пул
|
||||||
|
self.return_redis_connection(redis).await;
|
||||||
Ok(new_quota)
|
Ok(new_quota)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
40
src/auth.rs
40
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<Author, actix_web::Error> {
|
||||||
|
// Извлекаем токен из запроса
|
||||||
|
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 для пользователя
|
/// Сохраняет имя файла в Redis для пользователя
|
||||||
pub async fn user_added_file(
|
pub async fn user_added_file(
|
||||||
redis: Option<&mut MultiplexedConnection>,
|
redis: Option<&mut MultiplexedConnection>,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use actix_web::{HttpRequest, HttpResponse, Result, web};
|
|||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
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::handlers::MAX_USER_QUOTA_BYTES;
|
||||||
use crate::lookup::store_file_info;
|
use crate::lookup::store_file_info;
|
||||||
use crate::s3_utils::{self, generate_key_with_extension, upload_to_s3};
|
use crate::s3_utils::{self, generate_key_with_extension, upload_to_s3};
|
||||||
@@ -20,11 +20,8 @@ pub async fn upload_handler(
|
|||||||
mut payload: Multipart,
|
mut payload: Multipart,
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
) -> Result<HttpResponse, actix_web::Error> {
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
// Получаем Redis соединение для проверки сессий
|
// Универсальная аутентификация с connection pool
|
||||||
let mut redis_conn = state.redis.clone();
|
let author = authenticate_request_with_pool(&req, &state).await?;
|
||||||
|
|
||||||
// Универсальная аутентификация с извлечением токена и валидацией
|
|
||||||
let author = authenticate_request(&req, redis_conn.as_mut(), state.request_timeout).await?;
|
|
||||||
|
|
||||||
let user_id = &author.user_id;
|
let user_id = &author.user_id;
|
||||||
info!(
|
info!(
|
||||||
@@ -174,16 +171,26 @@ pub async fn upload_handler(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем информацию о файле в Redis
|
// Получаем соединение из пула для Redis операций
|
||||||
if let Err(e) = store_file_info(redis_conn.as_mut(), &filename, &content_type).await
|
if let Ok(mut redis_conn) = state.get_redis_connection().await {
|
||||||
{
|
// Сохраняем информацию о файле в Redis
|
||||||
error!("Failed to store file info in Redis: {}", e);
|
if let Err(e) =
|
||||||
// Не прерываем процесс, файл уже загружен в S3
|
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 {
|
if let Err(e) = user_added_file(Some(&mut redis_conn), user_id, &filename).await
|
||||||
error!("Failed to record user file association: {}", e);
|
{
|
||||||
// Не прерываем процесс
|
error!("Failed to record user file association: {}", e);
|
||||||
|
// Не прерываем процесс
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем соединение в пул
|
||||||
|
state.return_redis_connection(redis_conn).await;
|
||||||
|
} else {
|
||||||
|
warn!("Redis pool unavailable, skipping file metadata storage");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем маппинг пути
|
// Сохраняем маппинг пути
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use log::{error, info, warn};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
use crate::auth::{Author, authenticate_request};
|
use crate::auth::{Author, authenticate_request_with_pool};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct UserWithQuotaResponse {
|
pub struct UserWithQuotaResponse {
|
||||||
@@ -26,11 +26,8 @@ pub async fn get_current_user_handler(
|
|||||||
) -> Result<HttpResponse, actix_web::Error> {
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
info!("Getting user info for valid token");
|
info!("Getting user info for valid token");
|
||||||
|
|
||||||
// Получаем Redis соединение для проверки сессий
|
// Универсальная аутентификация с connection pool
|
||||||
let mut redis_conn = state.redis.clone();
|
let user = match authenticate_request_with_pool(&req, &state).await {
|
||||||
|
|
||||||
// Универсальная аутентификация с извлечением токена и валидацией
|
|
||||||
let user = match authenticate_request(&req, redis_conn.as_mut(), state.request_timeout).await {
|
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
info!(
|
info!(
|
||||||
"Successfully retrieved user info: user_id={}, username={:?}",
|
"Successfully retrieved user info: user_id={}, username={:?}",
|
||||||
|
|||||||
@@ -10,5 +10,8 @@ pub mod security;
|
|||||||
pub mod thumbnail;
|
pub mod thumbnail;
|
||||||
|
|
||||||
// Реэкспортируем основные типы для удобства
|
// Реэкспортируем основные типы для удобства
|
||||||
pub use app_state::AppState;
|
pub use app_state::{AppState, RedisConnectionPool};
|
||||||
pub use auth::{Author, authenticate_request, extract_token_from_request, secure_token_validation};
|
pub use auth::{
|
||||||
|
Author, authenticate_request, authenticate_request_with_pool, extract_token_from_request,
|
||||||
|
secure_token_validation,
|
||||||
|
};
|
||||||
|
|||||||
218
tests/redis_pool_test.rs
Normal file
218
tests/redis_pool_test.rs
Normal file
@@ -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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user