redis-connect-fix
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2677,6 +2677,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ aws-sdk-s3 = { version = "1.104.0", default-features = false, features = ["rt-to
|
||||
image = { version = "0.25.6", default-features = false, features = ["jpeg", "png", "webp", "tiff"] }
|
||||
mime_guess = "2.0.5"
|
||||
md5 = "0.7.0"
|
||||
url = "2.5.4"
|
||||
aws-config = { version = "1.8.6", default-features = false, features = ["rt-tokio", "rustls"] }
|
||||
actix-multipart = "0.7.2"
|
||||
log = "0.4.22"
|
||||
|
||||
@@ -23,21 +23,14 @@ impl AppState {
|
||||
/// Инициализация нового состояния приложения.
|
||||
pub async fn new() -> Self {
|
||||
let security_config = SecurityConfig::default();
|
||||
Self::new_with_config(security_config).await.unwrap_or_else(|e| {
|
||||
log::error!("❌ Failed to initialize AppState: {}", e);
|
||||
std::process::exit(1);
|
||||
})
|
||||
Self::new_with_config(security_config).await
|
||||
}
|
||||
|
||||
/// Инициализация с кастомной конфигурацией безопасности.
|
||||
pub async fn new_with_config(security_config: SecurityConfig) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
pub async fn new_with_config(security_config: SecurityConfig) -> Self {
|
||||
// Получаем конфигурацию для Redis с таймаутом
|
||||
let redis_url = env::var("REDIS_URL").expect("REDIS_URL must be set");
|
||||
log::info!("🔗 Attempting Redis connection to: {}", redis_url.replace(&redis_url.split('@').nth(0).unwrap_or(""), "***"));
|
||||
let redis_client = RedisClient::open(redis_url.clone()).map_err(|e| {
|
||||
log::error!("❌ Failed to parse Redis URL: {}", e);
|
||||
e
|
||||
})?;
|
||||
let redis_client = RedisClient::open(redis_url).expect("Invalid Redis URL");
|
||||
|
||||
// Устанавливаем таймаут для Redis операций с graceful fallback
|
||||
let redis_connection = match tokio::time::timeout(
|
||||
@@ -134,7 +127,7 @@ impl AppState {
|
||||
// Кэшируем список файлов из AWS при старте приложения
|
||||
app_state.cache_filelist().await;
|
||||
|
||||
Ok(app_state)
|
||||
app_state
|
||||
}
|
||||
|
||||
/// Кэширует список файлов из Storj S3 в Redis.
|
||||
|
||||
165
tests/redis_connection_test.rs
Normal file
165
tests/redis_connection_test.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use redis::{Client, AsyncCommands};
|
||||
use std::env;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_redis_url_parsing() {
|
||||
// Тестируем различные форматы Redis URL
|
||||
|
||||
let test_urls = vec![
|
||||
// Простой URL без пароля
|
||||
"redis://localhost:6379",
|
||||
// URL с паролем (должен использовать дефолтное имя пользователя 'redis')
|
||||
"redis://:password@localhost:6379",
|
||||
// URL с пользователем и паролем
|
||||
"redis://user:password@localhost:6379",
|
||||
// URL с длинным hex паролем (как в Dokku) - должен использовать дефолтное имя 'redis'
|
||||
"redis://:dbc5f9f9007c555e209964454c9d6abecae5f1db72e490acd5c94354dc012282@dokku-redis-discoursio-redis:6379",
|
||||
// URL с IP адресом
|
||||
"redis://172.17.0.3:6379",
|
||||
// URL с exposed портом
|
||||
"redis://localhost:12389",
|
||||
// Тест с явным указанием дефолтного пользователя 'redis'
|
||||
"redis://redis:password@localhost:6379",
|
||||
// Тест с пустым пользователем и паролем (должен работать как без аутентификации)
|
||||
"redis://:@localhost:6379",
|
||||
];
|
||||
|
||||
for url in test_urls {
|
||||
// Парсим URL для детального вывода
|
||||
if let Ok(parsed) = url::Url::parse(url) {
|
||||
println!("Testing Redis URL: {}", url.replace(&url.split('@').nth(0).unwrap_or(""), "***"));
|
||||
println!(" Host: {}", parsed.host_str().unwrap_or("none"));
|
||||
println!(" Port: {}", parsed.port().unwrap_or(0));
|
||||
println!(" Username: '{}'", parsed.username());
|
||||
println!(" Password: '{}'", if parsed.password().is_some() { "***" } else { "none" });
|
||||
} else {
|
||||
println!("Testing Redis URL: {}", url);
|
||||
}
|
||||
|
||||
match Client::open(url) {
|
||||
Ok(client) => {
|
||||
println!("✅ URL parsed successfully");
|
||||
|
||||
// Попробуем подключиться (с коротким таймаутом)
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
client.get_multiplexed_async_connection()
|
||||
).await {
|
||||
Ok(Ok(_)) => println!("✅ Connection successful"),
|
||||
Ok(Err(e)) => println!("❌ Connection failed: {}", e),
|
||||
Err(_) => println!("⏰ Connection timeout"),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ URL parsing failed: {}", e);
|
||||
}
|
||||
}
|
||||
println!("---");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_redis_connection_with_env() {
|
||||
// Тестируем с реальным REDIS_URL из окружения
|
||||
if let Ok(redis_url) = env::var("REDIS_URL") {
|
||||
println!("Testing with real REDIS_URL: {}",
|
||||
redis_url.replace(&redis_url.split('@').nth(0).unwrap_or(""), "***"));
|
||||
|
||||
// Парсим реальный URL для детального вывода
|
||||
if let Ok(parsed) = url::Url::parse(&redis_url) {
|
||||
println!(" Host: {}", parsed.host_str().unwrap_or("none"));
|
||||
println!(" Port: {}", parsed.port().unwrap_or(0));
|
||||
println!(" Username: '{}'", parsed.username());
|
||||
println!(" Password: '{}'", if parsed.password().is_some() { "***" } else { "none" });
|
||||
}
|
||||
|
||||
match Client::open(redis_url) {
|
||||
Ok(client) => {
|
||||
println!("✅ Real URL parsed successfully");
|
||||
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client.get_multiplexed_async_connection()
|
||||
).await {
|
||||
Ok(Ok(mut conn)) => {
|
||||
println!("✅ Real connection successful");
|
||||
|
||||
// Попробуем выполнить простую команду
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
conn.ping::<String>()
|
||||
).await {
|
||||
Ok(Ok(result)) => println!("✅ PING successful: {}", result),
|
||||
Ok(Err(e)) => println!("❌ PING failed: {}", e),
|
||||
Err(_) => println!("⏰ PING timeout"),
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => println!("❌ Real connection failed: {}", e),
|
||||
Err(_) => println!("⏰ Real connection timeout"),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ Real URL parsing failed: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("⚠️ REDIS_URL not set, skipping real connection test");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redis_url_components() {
|
||||
// Тестируем парсинг компонентов URL
|
||||
let test_url = "redis://:dbc5f9f9007c555e209964454c9d6abecae5f1db72e490acd5c94354dc012282@dokku-redis-discoursio-redis:6379";
|
||||
|
||||
if let Ok(parsed) = url::Url::parse(test_url) {
|
||||
println!("✅ URL parsed successfully");
|
||||
println!(" Scheme: {}", parsed.scheme());
|
||||
println!(" Host: {}", parsed.host_str().unwrap_or("none"));
|
||||
println!(" Port: {}", parsed.port().unwrap_or(0));
|
||||
println!(" Username: '{}'", parsed.username());
|
||||
println!(" Password: {}", if parsed.password().is_some() { "***" } else { "none" });
|
||||
println!(" Path: {}", parsed.path());
|
||||
|
||||
// Проверяем, что пустое имя пользователя означает дефолтное 'redis'
|
||||
if parsed.username().is_empty() && parsed.password().is_some() {
|
||||
println!(" ⚠️ Empty username with password - Redis client should use default 'redis' user");
|
||||
}
|
||||
} else {
|
||||
println!("❌ URL parsing failed");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_redis_default_username_behavior() {
|
||||
// Тестируем поведение Redis client с пустым именем пользователя
|
||||
println!("Testing Redis default username behavior...");
|
||||
|
||||
let test_cases = vec![
|
||||
("redis://:password@localhost:6379", "Empty username with password"),
|
||||
("redis://redis:password@localhost:6379", "Explicit redis username"),
|
||||
("redis://:@localhost:6379", "Empty username and password"),
|
||||
("redis://localhost:6379", "No authentication"),
|
||||
];
|
||||
|
||||
for (url, description) in test_cases {
|
||||
println!("\n--- {} ---", description);
|
||||
println!("URL: {}", url.replace(&url.split('@').nth(0).unwrap_or(""), "***"));
|
||||
|
||||
if let Ok(parsed) = url::Url::parse(url) {
|
||||
println!(" Parsed Username: '{}'", parsed.username());
|
||||
println!(" Parsed Password: {}", if parsed.password().is_some() { "***" } else { "none" });
|
||||
|
||||
// Redis client поведение:
|
||||
// - Если username пустой, но есть password -> использует дефолтное имя 'redis'
|
||||
// - Если username и password пустые -> без аутентификации
|
||||
if parsed.username().is_empty() && parsed.password().is_some() {
|
||||
println!(" Expected Redis behavior: Use default username 'redis'");
|
||||
} else if parsed.username().is_empty() && parsed.password().is_none() {
|
||||
println!(" Expected Redis behavior: No authentication");
|
||||
} else {
|
||||
println!(" Expected Redis behavior: Use explicit credentials");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user