use lazy_static::lazy_static; use redis::{Client, Cmd, Connection, RedisError, RedisResult}; use std::env; use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, Once}; /// Redis connection configuration builder /// /// This struct is used to build a Redis connection configuration. /// It follows the builder pattern to allow for flexible configuration. #[derive(Clone)] pub struct RedisConfigBuilder { pub host: String, pub port: u16, pub db: i64, pub username: Option, pub password: Option, pub use_tls: bool, pub use_unix_socket: bool, pub socket_path: Option, pub connection_timeout: Option, } impl Default for RedisConfigBuilder { fn default() -> Self { Self { host: "127.0.0.1".to_string(), port: 6379, db: 0, username: None, password: None, use_tls: false, use_unix_socket: false, socket_path: None, connection_timeout: None, } } } impl RedisConfigBuilder { /// Create a new Redis connection configuration builder with default values pub fn new() -> Self { Self::default() } /// Set the host for the Redis connection pub fn host(mut self, host: &str) -> Self { self.host = host.to_string(); self } /// Set the port for the Redis connection pub fn port(mut self, port: u16) -> Self { self.port = port; self } /// Set the database for the Redis connection pub fn db(mut self, db: i64) -> Self { self.db = db; self } /// Set the username for the Redis connection (Redis 6.0+) pub fn username(mut self, username: &str) -> Self { self.username = Some(username.to_string()); self } /// Set the password for the Redis connection pub fn password(mut self, password: &str) -> Self { self.password = Some(password.to_string()); self } /// Enable TLS for the Redis connection pub fn use_tls(mut self, use_tls: bool) -> Self { self.use_tls = use_tls; self } /// Use Unix socket for the Redis connection pub fn use_unix_socket(mut self, use_unix_socket: bool) -> Self { self.use_unix_socket = use_unix_socket; self } /// Set the Unix socket path for the Redis connection pub fn socket_path(mut self, socket_path: &str) -> Self { self.socket_path = Some(socket_path.to_string()); self.use_unix_socket = true; self } /// Set the connection timeout in seconds pub fn connection_timeout(mut self, seconds: u64) -> Self { self.connection_timeout = Some(seconds); self } /// Build the connection URL from the configuration pub fn build_connection_url(&self) -> String { if self.use_unix_socket { if let Some(ref socket_path) = self.socket_path { return format!("unix://{}", socket_path); } else { // Default socket path let home_dir = env::var("HOME").unwrap_or_else(|_| String::from("/root")); return format!("unix://{}/hero/var/myredis.sock", home_dir); } } let mut url = if self.use_tls { format!("rediss://{}:{}", self.host, self.port) } else { format!("redis://{}:{}", self.host, self.port) }; // Add authentication if provided if let Some(ref username) = self.username { if let Some(ref password) = self.password { url = format!( "redis://{}:{}@{}:{}", username, password, self.host, self.port ); } else { url = format!("redis://{}@{}:{}", username, self.host, self.port); } } else if let Some(ref password) = self.password { url = format!("redis://:{}@{}:{}", password, self.host, self.port); } // Add database url = format!("{}/{}", url, self.db); url } /// Build a Redis client from the configuration pub fn build(&self) -> RedisResult<(Client, i64)> { let url = self.build_connection_url(); let client = Client::open(url)?; Ok((client, self.db)) } } // Global Redis client instance using lazy_static lazy_static! { static ref REDIS_CLIENT: Mutex>> = Mutex::new(None); static ref INIT: Once = Once::new(); } // Wrapper for Redis client to handle connection and DB selection pub struct RedisClientWrapper { client: Client, connection: Mutex>, db: i64, initialized: AtomicBool, } impl RedisClientWrapper { // Create a new Redis client wrapper fn new(client: Client, db: i64) -> Self { RedisClientWrapper { client, connection: Mutex::new(None), db, initialized: AtomicBool::new(false), } } // Execute a command on the Redis connection pub fn execute(&self, cmd: &mut Cmd) -> RedisResult { let mut conn_guard = self.connection.lock().unwrap(); // If we don't have a connection or it's not working, create a new one if conn_guard.is_none() || { if let Some(ref mut conn) = *conn_guard { let ping_result: RedisResult = redis::cmd("PING").query(conn); ping_result.is_err() } else { true } } { *conn_guard = Some(self.client.get_connection()?); } cmd.query(&mut conn_guard.as_mut().unwrap()) } // Initialize the client (ping and select DB) fn initialize(&self) -> RedisResult<()> { if self.initialized.load(Ordering::Relaxed) { return Ok(()); } let mut conn = self.client.get_connection()?; // Ping Redis to ensure it works let ping_result: String = redis::cmd("PING").query(&mut conn)?; if ping_result != "PONG" { return Err(RedisError::from(( redis::ErrorKind::ResponseError, "Failed to ping Redis server", ))); } // Select the database let _ = redis::cmd("SELECT").arg(self.db).exec(&mut conn); self.initialized.store(true, Ordering::Relaxed); // Store the connection let mut conn_guard = self.connection.lock().unwrap(); *conn_guard = Some(conn); Ok(()) } } // Get the Redis client instance pub fn get_redis_client() -> RedisResult> { // Check if we already have a client { let guard = REDIS_CLIENT.lock().unwrap(); if let Some(ref client) = &*guard { return Ok(Arc::clone(client)); } } // Create a new client let client = create_redis_client()?; // Store the client globally { let mut guard = REDIS_CLIENT.lock().unwrap(); *guard = Some(Arc::clone(&client)); } Ok(client) } // Create a new Redis client fn create_redis_client() -> RedisResult> { // Get Redis configuration from environment variables let db = get_redis_db(); let password = env::var("REDIS_PASSWORD").ok(); let username = env::var("REDIS_USERNAME").ok(); let host = env::var("REDIS_HOST").unwrap_or_else(|_| String::from("127.0.0.1")); let port = env::var("REDIS_PORT") .ok() .and_then(|p| p.parse::().ok()) .unwrap_or(6379); // Create a builder with environment variables let mut builder = RedisConfigBuilder::new().host(&host).port(port).db(db); if let Some(user) = username { builder = builder.username(&user); } if let Some(pass) = password { builder = builder.password(&pass); } // First try: Connect via Unix socket if it exists let home_dir = env::var("HOME").unwrap_or_else(|_| String::from("/root")); let socket_path = format!("{}/hero/var/myredis.sock", home_dir); if Path::new(&socket_path).exists() { // Try to connect via Unix socket let socket_builder = builder.clone().socket_path(&socket_path); match socket_builder.build() { Ok((client, db)) => { let wrapper = Arc::new(RedisClientWrapper::new(client, db)); // Initialize the client if let Err(err) = wrapper.initialize() { eprintln!( "Socket exists at {} but connection failed: {}", socket_path, err ); } else { return Ok(wrapper); } } Err(err) => { eprintln!( "Socket exists at {} but connection failed: {}", socket_path, err ); } } } // Second try: Connect via TCP match builder.clone().build() { Ok((client, db)) => { let wrapper = Arc::new(RedisClientWrapper::new(client, db)); // Initialize the client wrapper.initialize()?; Ok(wrapper) } Err(err) => Err(RedisError::from(( redis::ErrorKind::IoError, "Failed to connect to Redis", format!( "Could not connect via socket at {} or via TCP to {}:{}: {}", socket_path, host, port, err ), ))), } } // Get the Redis DB number from environment variable fn get_redis_db() -> i64 { env::var("REDISDB") .ok() .and_then(|db_str| db_str.parse::().ok()) .unwrap_or(0) } // Reload the Redis client pub fn reset() -> RedisResult<()> { // Clear the existing client { let mut client_guard = REDIS_CLIENT.lock().unwrap(); *client_guard = None; } // Create a new client, only return error if it fails // We don't need to return the client itself get_redis_client()?; Ok(()) } // Execute a Redis command pub fn execute(cmd: &mut Cmd) -> RedisResult where T: redis::FromRedisValue, { let client = get_redis_client()?; client.execute(cmd) } /// Create a new Redis client with custom configuration /// /// # Arguments /// /// * `config` - The Redis connection configuration builder /// /// # Returns /// /// * `RedisResult` - The Redis client if successful, error otherwise pub fn with_config(config: RedisConfigBuilder) -> RedisResult { let (client, _) = config.build()?; Ok(client) }