use super::circuit_breaker::CircuitBreaker;
use super::client::RedisBackend;
use super::error::map_redis_error;
use crate::config::DistributedConfig;
use crate::core::RedisModeType;
use crate::error::{OxCacheError, OxCacheResult};
use std::sync::Arc;
use std::time::Duration;
pub type RedisMode = RedisModeType;
#[derive(Debug)]
pub struct RedisBackendBuilder {
connection_string: Option<String>,
mode: RedisMode,
pool_size: usize,
connection_timeout: Duration,
retry_count: u32,
retry_delay: Duration,
circuit_breaker_threshold: u32,
circuit_breaker_reset_timeout: Duration,
database: Option<u16>,
pub(crate) dangerous_clear_enabled: bool,
}
impl Default for RedisBackendBuilder {
fn default() -> Self {
Self {
connection_string: None,
mode: RedisMode::default(),
pool_size: 8,
connection_timeout: Duration::from_secs(2),
retry_count: 3,
retry_delay: Duration::from_millis(100),
circuit_breaker_threshold: 5,
circuit_breaker_reset_timeout: Duration::from_secs(30),
database: None,
dangerous_clear_enabled: false,
}
}
}
impl RedisBackendBuilder {
pub fn connection_string(mut self, connection_string: &str) -> Self {
self.connection_string = Some(connection_string.to_string());
self
}
pub fn mode(mut self, mode: RedisMode) -> Self {
self.mode = mode;
self
}
pub fn pool_size(mut self, pool_size: usize) -> Self {
self.pool_size = pool_size;
self
}
pub fn connection_timeout(mut self, timeout: Duration) -> Self {
self.connection_timeout = timeout;
self
}
pub fn retry_count(mut self, count: u32) -> Self {
self.retry_count = count;
self
}
pub fn retry_delay(mut self, delay: Duration) -> Self {
self.retry_delay = delay;
self
}
pub fn database(mut self, db: u16) -> Self {
self.database = Some(db);
self
}
pub fn circuit_breaker_threshold(mut self, threshold: u32) -> Self {
self.circuit_breaker_threshold = threshold;
self
}
pub fn circuit_breaker_reset_timeout(mut self, timeout: Duration) -> Self {
self.circuit_breaker_reset_timeout = timeout;
self
}
pub fn distributed_config(mut self, config: DistributedConfig) -> Self {
self.retry_count = config.retry_count;
self.retry_delay = config.retry_base_delay;
self.circuit_breaker_threshold = config.circuit_breaker_threshold;
self.circuit_breaker_reset_timeout = config.circuit_breaker_reset_timeout;
self
}
pub fn dangerous_clear_enabled(mut self, enabled: bool) -> Self {
self.dangerous_clear_enabled = enabled;
self
}
pub async fn build(self) -> OxCacheResult<RedisBackend> {
if self.pool_size == 0 {
return Err(OxCacheError::InvalidInput(
"Connection pool size must be at least 1".to_string(),
));
}
let mut connection_string = self
.connection_string
.ok_or_else(|| OxCacheError::InvalidInput("Connection string is required".to_string()))?;
if let Some(db) = self.database {
connection_string = connection_string.trim_end_matches('/').to_string();
connection_string.push('/');
connection_string.push_str(&db.to_string());
}
if !connection_string.starts_with("rediss://") {
let allow_insecure = std::env::var("OXCACHE_ALLOW_INSECURE_REDIS")
.map(|v| v == "I_UNDERSTAND_THE_RISKS" || v == "development-only")
.unwrap_or(false);
if !allow_insecure {
return Err(OxCacheError::InvalidInput(
"Redis connection must use TLS (rediss://) in production. \
To allow insecure connections for development only, \
set OXCACHE_ALLOW_INSECURE_REDIS=I_UNDERSTAND_THE_RISKS"
.to_string(),
));
}
}
let client = redis::Client::open(connection_string).map_err(map_redis_error)?;
let connection_result = tokio::time::timeout(self.connection_timeout, client.get_connection_manager()).await;
let connection_manager = match connection_result {
Ok(Ok(mgr)) => mgr,
Ok(Err(e)) => {
return Err(OxCacheError::Connection(format!("Failed to connect to Redis: {}", e)));
}
Err(_) => {
return Err(OxCacheError::Connection(
"Connection timeout - Redis server unavailable".to_string(),
));
}
};
Ok(RedisBackend::from_parts(
std::sync::Arc::new(client),
self.mode,
connection_manager,
self.dangerous_clear_enabled,
self.retry_count,
self.retry_delay,
Arc::new(CircuitBreaker::new(
self.circuit_breaker_threshold,
self.circuit_breaker_reset_timeout,
)),
))
}
}