use crate::config::legacy_config::{
CacheWarmupConfig, ClusterConfig, InvalidationChannelConfig, SentinelConfig, SerializationType,
};
use secrecy::SecretString;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum CacheType {
L1,
L2,
#[default]
TwoLevel,
}
impl fmt::Display for CacheType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CacheType::L1 => write!(f, "l1"),
CacheType::L2 => write!(f, "l2"),
CacheType::TwoLevel => write!(f, "two-level"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum RedisMode {
#[default]
Standalone,
Sentinel,
Cluster,
}
impl fmt::Display for RedisMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RedisMode::Standalone => write!(f, "standalone"),
RedisMode::Sentinel => write!(f, "sentinel"),
RedisMode::Cluster => write!(f, "cluster"),
}
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct ServiceConfig {
pub cache_type: CacheType,
pub ttl: Option<u64>,
pub serialization: Option<SerializationType>,
#[cfg(feature = "l1-moka")]
pub l1: Option<L1Config>,
#[cfg(feature = "l2-redis")]
pub l2: Option<L2Config>,
#[cfg(feature = "l2-redis")]
pub two_level: Option<TwoLevelConfig>,
}
impl fmt::Debug for ServiceConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ServiceConfig")
.field("cache_type", &self.cache_type)
.field("ttl", &self.ttl)
.field("serialization", &self.serialization)
.field("l1", &self.l1)
.finish()
}
}
impl ServiceConfig {
#[cfg(feature = "l1-moka")]
pub fn l1_only() -> Self {
Self {
cache_type: CacheType::L1,
ttl: None,
serialization: None,
l1: Some(L1Config::default()),
#[cfg(feature = "l2-redis")]
l2: None,
#[cfg(feature = "l2-redis")]
two_level: None,
}
}
#[cfg(not(feature = "l1-moka"))]
pub fn l1_only() -> Self {
Self {
cache_type: CacheType::L1,
ttl: None,
serialization: None,
}
}
#[cfg(feature = "l2-redis")]
pub fn l2_only() -> Self {
Self {
cache_type: CacheType::L2,
ttl: None,
serialization: None,
l1: None,
l2: Some(L2Config::default()),
two_level: None,
}
}
#[cfg(all(not(feature = "l2-redis"), feature = "l1-moka"))]
pub fn l2_only() -> Self {
Self {
cache_type: CacheType::L2,
ttl: None,
serialization: None,
l1: None,
}
}
#[cfg(all(not(feature = "l2-redis"), not(feature = "l1-moka")))]
pub fn l2_only() -> Self {
Self {
cache_type: CacheType::L2,
ttl: None,
serialization: None,
}
}
#[cfg(all(feature = "l1-moka", feature = "l2-redis"))]
pub fn two_level() -> Self {
Self {
cache_type: CacheType::TwoLevel,
ttl: None,
serialization: None,
l1: Some(L1Config::default()),
l2: Some(L2Config::default()),
two_level: Some(TwoLevelConfig::default()),
}
}
#[cfg(all(feature = "l2-redis", not(feature = "l1-moka")))]
pub fn two_level() -> Self {
Self {
cache_type: CacheType::TwoLevel,
ttl: None,
serialization: None,
l1: None,
l2: Some(L2Config::default()),
two_level: Some(TwoLevelConfig::default()),
}
}
#[cfg(all(feature = "l1-moka", not(feature = "l2-redis")))]
pub fn two_level() -> Self {
Self {
cache_type: CacheType::TwoLevel,
ttl: None,
serialization: None,
l1: Some(L1Config::default()),
}
}
#[cfg(not(any(feature = "l1-moka", feature = "l2-redis")))]
pub fn two_level() -> Self {
Self {
cache_type: CacheType::TwoLevel,
ttl: None,
serialization: None,
}
}
pub fn with_cache_type(cache_type: CacheType) -> Self {
match cache_type {
CacheType::L1 => Self::l1_only(),
CacheType::L2 => Self::l2_only(),
CacheType::TwoLevel => Self::two_level(),
}
}
pub fn with_ttl(mut self, ttl: u64) -> Self {
self.ttl = Some(ttl);
self
}
#[cfg(feature = "l1-moka")]
pub fn with_l1(mut self, l1: L1Config) -> Self {
self.l1 = Some(l1);
self
}
#[cfg(feature = "l2-redis")]
pub fn with_l2(mut self, l2: L2Config) -> Self {
self.l2 = Some(l2);
self
}
#[cfg(feature = "l2-redis")]
pub fn with_two_level(mut self, two_level: TwoLevelConfig) -> Self {
self.two_level = Some(two_level);
self
}
pub fn can_use_l1(&self) -> bool {
cfg!(feature = "l1-moka")
}
pub fn can_use_l2(&self) -> bool {
cfg!(feature = "l2-redis")
}
pub fn can_use_two_level(&self) -> bool {
cfg!(feature = "l1-moka") && cfg!(feature = "l2-redis")
}
}
#[cfg(feature = "l1-moka")]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct L1Config {
pub max_capacity: u64,
pub max_key_length: usize,
pub max_value_size: usize,
pub cleanup_interval_secs: u64,
}
#[cfg(feature = "l1-moka")]
impl Default for L1Config {
fn default() -> Self {
Self {
max_capacity: 10000,
max_key_length: 512,
max_value_size: 1024 * 1024, cleanup_interval_secs: 300,
}
}
}
#[cfg(feature = "l1-moka")]
impl L1Config {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_capacity(mut self, capacity: u64) -> Self {
self.max_capacity = capacity;
self
}
pub fn with_max_key_length(mut self, length: usize) -> Self {
self.max_key_length = length;
self
}
pub fn with_max_value_size(mut self, size: usize) -> Self {
self.max_value_size = size;
self
}
pub fn with_cleanup_interval_secs(mut self, secs: u64) -> Self {
self.cleanup_interval_secs = secs;
self
}
}
#[cfg(feature = "l2-redis")]
#[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct L2Config {
pub mode: RedisMode,
#[serde(skip)]
pub connection_string: SecretString,
pub connection_timeout_ms: u64,
pub command_timeout_ms: u64,
#[serde(skip)]
pub password: Option<SecretString>,
pub enable_tls: bool,
pub sentinel: Option<SentinelConfig>,
pub cluster: Option<ClusterConfig>,
pub default_ttl: Option<u64>,
pub max_key_length: usize,
pub max_value_size: usize,
}
#[cfg(feature = "l2-redis")]
impl fmt::Debug for L2Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("L2Config")
.field("mode", &self.mode)
.field("connection_string", &"[REDACTED]")
.field("connection_timeout_ms", &self.connection_timeout_ms)
.field("command_timeout_ms", &self.command_timeout_ms)
.field("password", &"[REDACTED]")
.field("enable_tls", &self.enable_tls)
.field("sentinel", &self.sentinel)
.field("cluster", &self.cluster)
.field("default_ttl", &self.default_ttl)
.field("max_key_length", &self.max_key_length)
.field("max_value_size", &self.max_value_size)
.finish()
}
}
#[cfg(feature = "l2-redis")]
impl L2Config {
pub fn new() -> Self {
Self::default()
}
pub fn with_connection_string(mut self, connection_string: &str) -> Self {
self.connection_string = SecretString::new(connection_string.to_string());
self
}
pub fn with_mode(mut self, mode: RedisMode) -> Self {
self.mode = mode;
self
}
pub fn with_default_ttl(mut self, ttl: u64) -> Self {
self.default_ttl = Some(ttl);
self
}
pub fn with_password(mut self, password: &str) -> Self {
self.password = Some(SecretString::new(password.to_string()));
self
}
pub fn with_connection_timeout_ms(mut self, timeout: u64) -> Self {
self.connection_timeout_ms = timeout;
self
}
pub fn with_command_timeout_ms(mut self, timeout: u64) -> Self {
self.command_timeout_ms = timeout;
self
}
pub fn with_enable_tls(mut self, enable: bool) -> Self {
self.enable_tls = enable;
self
}
pub fn with_sentinel(mut self, sentinel: SentinelConfig) -> Self {
self.sentinel = Some(sentinel);
self
}
pub fn with_cluster(mut self, cluster: ClusterConfig) -> Self {
self.cluster = Some(cluster);
self
}
}
#[cfg(feature = "l2-redis")]
impl Default for L2Config {
fn default() -> Self {
Self {
mode: RedisMode::default(),
connection_string: SecretString::new("".to_string()),
connection_timeout_ms: 5000,
command_timeout_ms: 30000,
password: None,
enable_tls: false,
sentinel: None,
cluster: None,
default_ttl: None,
max_key_length: 512,
max_value_size: 10 * 1024 * 1024,
}
}
}
#[cfg(feature = "l2-redis")]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct TwoLevelConfig {
pub promote_on_hit: bool,
pub enable_batch_write: bool,
pub batch_size: usize,
pub batch_interval_ms: u64,
pub max_key_length: Option<usize>,
pub max_value_size: Option<usize>,
pub bloom_filter: Option<BloomFilterConfig>,
pub invalidation_channel: Option<InvalidationChannelConfig>,
pub warmup: Option<CacheWarmupConfig>,
}
#[cfg(feature = "l2-redis")]
impl TwoLevelConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_enable_batch_write(mut self, enable: bool) -> Self {
self.enable_batch_write = enable;
self
}
pub fn with_batch_size(mut self, size: usize) -> Self {
self.batch_size = size;
self
}
pub fn with_batch_interval_ms(mut self, ms: u64) -> Self {
self.batch_interval_ms = ms;
self
}
pub fn with_promote_on_hit(mut self, promote: bool) -> Self {
self.promote_on_hit = promote;
self
}
pub fn with_max_key_length(mut self, length: usize) -> Self {
self.max_key_length = Some(length);
self
}
pub fn with_max_value_size(mut self, size: usize) -> Self {
self.max_value_size = Some(size);
self
}
}
#[cfg(all(feature = "l2-redis", feature = "bloom-filter"))]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BloomFilterConfig {
pub expected_elements: usize,
pub false_positive_rate: f64,
pub auto_add_keys: bool,
pub name: String,
}
#[cfg(all(feature = "l2-redis", feature = "bloom-filter"))]
impl BloomFilterConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_expected_elements(mut self, elements: usize) -> Self {
self.expected_elements = elements;
self
}
pub fn with_false_positive_rate(mut self, rate: f64) -> Self {
self.false_positive_rate = rate;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_config_l1_only() {
#[cfg(feature = "l1-moka")]
{
let config = ServiceConfig::l1_only();
assert_eq!(config.cache_type, CacheType::L1);
assert!(config.l1.is_some());
#[cfg(feature = "l2-redis")]
assert!(config.l2.is_none());
}
}
#[test]
fn test_service_config_l2_only() {
#[cfg(feature = "l2-redis")]
{
let config = ServiceConfig::l2_only();
assert_eq!(config.cache_type, CacheType::L2);
assert!(config.l2.is_some());
#[cfg(feature = "l1-moka")]
assert!(config.l1.is_none());
}
}
#[test]
fn test_service_config_two_level() {
let config = ServiceConfig::two_level();
assert_eq!(config.cache_type, CacheType::TwoLevel);
#[cfg(feature = "l1-moka")]
assert!(config.l1.is_some());
#[cfg(feature = "l2-redis")]
assert!(config.l2.is_some());
}
#[test]
fn test_service_config_with_ttl() {
let config = ServiceConfig::two_level().with_ttl(600);
assert_eq!(config.ttl, Some(600));
}
#[test]
fn test_l2_config_with_connection() {
#[cfg(feature = "l2-redis")]
{
let config = L2Config::new()
.with_connection_string("redis://localhost:6379")
.with_default_ttl(7200);
assert_eq!(config.default_ttl, Some(7200));
}
}
#[test]
fn test_two_level_config_batch() {
#[cfg(feature = "l2-redis")]
{
let config = TwoLevelConfig::new()
.with_enable_batch_write(true)
.with_batch_size(500);
assert!(config.enable_batch_write);
assert_eq!(config.batch_size, 500);
}
}
#[test]
fn test_service_config_feature_flags() {
let config = ServiceConfig::default();
#[cfg(feature = "l1-moka")]
assert!(config.can_use_l1());
#[cfg(feature = "l2-redis")]
assert!(config.can_use_l2());
}
}