use std::time::Duration;
use crate::cache_redis::RedisBreakerConfig;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RedisCacheConfig {
pub url: String,
pub namespace: String,
pub default_ttl: Duration,
pub not_found_ttl: Duration,
pub connect_timeout: Duration,
pub command_timeout: Duration,
pub delete_retry: RedisDeleteRetryConfig,
pub unavailable_policy: RedisUnavailablePolicy,
pub breaker: RedisBreakerConfig,
pub cluster: RedisClusterConfig,
}
impl Default for RedisCacheConfig {
fn default() -> Self {
Self {
url: "redis://127.0.0.1:6379".to_string(),
namespace: "rs-zero".to_string(),
default_ttl: Duration::from_secs(300),
not_found_ttl: Duration::from_secs(60),
connect_timeout: Duration::from_secs(3),
command_timeout: Duration::from_secs(2),
delete_retry: RedisDeleteRetryConfig::default(),
unavailable_policy: RedisUnavailablePolicy::default(),
breaker: RedisBreakerConfig::default(),
cluster: RedisClusterConfig::default(),
}
}
}
impl RedisCacheConfig {
pub fn validate(&self) -> Result<(), crate::cache_redis::RedisCacheError> {
if self.cluster.enabled {
validate_redis_urls(&self.url)?;
} else {
validate_redis_url(&self.url)?;
}
if self.namespace.trim().is_empty() {
return Err(crate::cache_redis::RedisCacheError::InvalidConfig(
"redis cache namespace is required".to_string(),
));
}
Ok(())
}
}
fn validate_redis_url(url: &str) -> Result<(), crate::cache_redis::RedisCacheError> {
let url = url.trim();
if url.is_empty() || url.contains(',') {
return Err(crate::cache_redis::RedisCacheError::InvalidUrl {
url: url.to_string(),
});
}
validate_redis_url_scheme(url)
}
fn validate_redis_urls(urls: &str) -> Result<(), crate::cache_redis::RedisCacheError> {
let nodes = urls
.split(',')
.map(str::trim)
.filter(|url| !url.is_empty())
.collect::<Vec<_>>();
if nodes.is_empty() {
return Err(crate::cache_redis::RedisCacheError::InvalidUrl {
url: urls.to_string(),
});
}
for url in nodes {
validate_redis_url_scheme(url)?;
}
Ok(())
}
fn validate_redis_url_scheme(url: &str) -> Result<(), crate::cache_redis::RedisCacheError> {
if !url.starts_with("redis://") && !url.starts_with("rediss://") {
return Err(crate::cache_redis::RedisCacheError::InvalidUrl {
url: url.to_string(),
});
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RedisOperation {
Get,
Set,
Delete,
}
impl RedisOperation {
pub fn as_str(self) -> &'static str {
match self {
Self::Get => "get",
Self::Set => "set",
Self::Delete => "delete",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RedisDegradedAction {
ReturnError,
ReturnMiss,
SkipWrite,
}
impl RedisDegradedAction {
pub fn as_str(self) -> &'static str {
match self {
Self::ReturnError => "return_error",
Self::ReturnMiss => "return_miss",
Self::SkipWrite => "skip_write",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RedisUnavailablePolicy {
pub get: RedisDegradedAction,
pub set: RedisDegradedAction,
pub delete: RedisDegradedAction,
}
impl Default for RedisUnavailablePolicy {
fn default() -> Self {
Self::fail_closed()
}
}
impl RedisUnavailablePolicy {
pub fn fail_closed() -> Self {
Self {
get: RedisDegradedAction::ReturnError,
set: RedisDegradedAction::ReturnError,
delete: RedisDegradedAction::ReturnError,
}
}
pub fn fail_open_for_cache() -> Self {
Self {
get: RedisDegradedAction::ReturnMiss,
set: RedisDegradedAction::SkipWrite,
delete: RedisDegradedAction::SkipWrite,
}
}
pub fn action_for(
&self,
operation: RedisOperation,
_error: &crate::cache_redis::RedisCacheError,
) -> RedisDegradedAction {
match operation {
RedisOperation::Get => self.get,
RedisOperation::Set => self.set,
RedisOperation::Delete => self.delete,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RedisDegradationEvent {
pub operation: RedisOperation,
pub action: RedisDegradedAction,
}
impl RedisDegradationEvent {
pub fn new(operation: RedisOperation, action: RedisDegradedAction) -> Self {
Self { operation, action }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RedisClusterConfig {
pub enabled: bool,
pub max_redirects: u32,
pub read_from_replicas: bool,
}
impl Default for RedisClusterConfig {
fn default() -> Self {
Self {
enabled: false,
max_redirects: 5,
read_from_replicas: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RedisDeleteRetryConfig {
pub enabled: bool,
pub capacity: usize,
pub max_attempts: u32,
pub initial_delay: Duration,
}
impl Default for RedisDeleteRetryConfig {
fn default() -> Self {
Self {
enabled: false,
capacity: 1024,
max_attempts: 3,
initial_delay: Duration::from_millis(50),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RedisNodeConfig {
pub name: String,
pub url: String,
pub weight: u32,
}
impl RedisNodeConfig {
pub fn new(name: impl Into<String>, url: impl Into<String>) -> Self {
Self {
name: name.into(),
url: url.into(),
weight: 1,
}
}
pub fn with_weight(mut self, weight: u32) -> Self {
self.weight = weight;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RedisShardedCacheConfig {
pub namespace: String,
pub default_ttl: Duration,
pub not_found_ttl: Duration,
pub connect_timeout: Duration,
pub command_timeout: Duration,
pub nodes: Vec<RedisNodeConfig>,
pub delete_retry: RedisDeleteRetryConfig,
pub breaker: RedisBreakerConfig,
}
impl Default for RedisShardedCacheConfig {
fn default() -> Self {
let base = RedisCacheConfig::default();
Self {
namespace: base.namespace,
default_ttl: base.default_ttl,
not_found_ttl: base.not_found_ttl,
connect_timeout: base.connect_timeout,
command_timeout: base.command_timeout,
nodes: Vec::new(),
delete_retry: RedisDeleteRetryConfig::default(),
breaker: RedisBreakerConfig::default(),
}
}
}
impl RedisShardedCacheConfig {
pub fn validate(&self) -> Result<(), crate::cache_redis::RedisCacheError> {
if self.namespace.trim().is_empty() {
return Err(crate::cache_redis::RedisCacheError::InvalidConfig(
"redis cache namespace is required".to_string(),
));
}
if self.nodes.is_empty() {
return Err(crate::cache_redis::RedisCacheError::InvalidConfig(
"at least one redis shard is required".to_string(),
));
}
for node in &self.nodes {
self.validate_node(node)?;
}
Ok(())
}
pub fn node_cache_config(&self, node: &RedisNodeConfig) -> RedisCacheConfig {
RedisCacheConfig {
url: node.url.clone(),
namespace: self.namespace.clone(),
default_ttl: self.default_ttl,
not_found_ttl: self.not_found_ttl,
connect_timeout: self.connect_timeout,
command_timeout: self.command_timeout,
delete_retry: self.delete_retry,
unavailable_policy: RedisUnavailablePolicy::default(),
breaker: self.breaker.clone(),
cluster: RedisClusterConfig::default(),
}
}
fn validate_node(
&self,
node: &RedisNodeConfig,
) -> Result<(), crate::cache_redis::RedisCacheError> {
if node.name.trim().is_empty() {
return Err(crate::cache_redis::RedisCacheError::InvalidConfig(
"redis shard name is required".to_string(),
));
}
if node.weight == 0 {
return Err(crate::cache_redis::RedisCacheError::InvalidConfig(format!(
"redis shard `{}` weight must be greater than zero",
node.name
)));
}
self.node_cache_config(node).validate()
}
}