use std::path::{Path, PathBuf};
use std::time::Duration;
use serde::{Deserialize, Serialize};
mod geometry;
mod validation;
use crate::Result;
use crate::cuda::CudaConfig;
use geometry::{CacheGeometryDetector, DefaultShardCount, HotTierCapacity};
use validation::ConfigValidator;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct FastCacheConfig {
pub bind_addr: String,
pub max_connections: usize,
pub shard_count: usize,
pub max_memory_bytes: u64,
pub eviction_policy: EvictionPolicy,
pub ttl_sweep_interval_ms: u64,
pub stats_interval_ms: u64,
pub tiers: TierConfig,
pub cuda: CudaConfig,
pub persistence: PersistenceConfig,
pub replication: ReplicationConfig,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum EvictionPolicy {
#[default]
None,
Lru,
Lfu,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TierConfig {
pub hot_capacity: usize,
pub warm_capacity: usize,
pub cold_capacity: usize,
pub promotion_batch: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PersistenceConfig {
pub enabled: bool,
pub data_dir: PathBuf,
pub segment_size_bytes: u64,
pub fsync_interval_ms: u64,
pub snapshot_every_seconds: u64,
pub snapshot_min_writes: u64,
pub compress_snapshots: bool,
pub compress_wal: bool,
pub wal_channel_capacity: usize,
pub tcp_export: WalTcpExportConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct WalTcpExportConfig {
pub enabled: bool,
pub mode: WalTcpExportMode,
pub addr: String,
pub auth_token: Option<String>,
pub channel_capacity: usize,
pub max_subscribers: usize,
pub connect_timeout_ms: u64,
pub write_timeout_ms: u64,
pub reconnect_backoff_ms: u64,
pub backpressure_on_full: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ReplicationConfig {
pub enabled: bool,
pub role: ReplicationRole,
pub bind_addr: String,
pub replica_of: Option<String>,
pub auth_token: Option<String>,
pub compression: ReplicationCompression,
pub zstd_level: i32,
pub send_policy: ReplicationSendPolicy,
pub batch_max_records: usize,
pub batch_max_bytes: usize,
pub batch_max_delay_us: u64,
pub backlog_bytes: usize,
pub snapshot_chunk_bytes: usize,
pub queue_capacity: usize,
pub max_replicas: usize,
pub connect_timeout_ms: u64,
pub write_timeout_ms: u64,
pub reconnect_backoff_ms: u64,
pub subscriber_channel_capacity: usize,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReplicationRole {
#[default]
Primary,
Replica,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReplicationCompression {
None,
#[default]
Zstd,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReplicationSendPolicy {
Immediate,
#[default]
Batch,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WalTcpExportMode {
Connect,
Listen,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct CacheGeometry {
pub l1d_bytes: usize,
pub l2_bytes: usize,
pub l3_bytes: usize,
}
struct ConfigFile<'a> {
path: &'a Path,
}
enum ConfigFileParent<'a> {
Present(&'a Path),
Missing,
}
impl Default for FastCacheConfig {
fn default() -> Self {
let shard_count = Self::default_shard_count();
Self {
bind_addr: "127.0.0.1:6380".to_string(),
max_connections: 4_096,
shard_count,
max_memory_bytes: 0,
eviction_policy: EvictionPolicy::None,
ttl_sweep_interval_ms: 1_000,
stats_interval_ms: 5_000,
tiers: TierConfig::from_geometry(CacheGeometry::detect_current_host(), shard_count),
cuda: CudaConfig::default(),
persistence: PersistenceConfig::default(),
replication: ReplicationConfig::default(),
}
}
}
impl Default for TierConfig {
fn default() -> Self {
Self::from_geometry(
CacheGeometry::detect_current_host(),
FastCacheConfig::default_shard_count(),
)
}
}
impl TierConfig {
pub fn from_geometry(geometry: CacheGeometry, shard_count: usize) -> Self {
let hot_capacity = HotTierCapacity::from_l1(geometry.l1d_bytes);
let warm_capacity = (geometry.l2_bytes / 160).clamp(1_024, 131_072);
let cold_bytes_per_shard = usize::max(
geometry.l3_bytes / usize::max(shard_count, 1),
2 * 1024 * 1024,
);
let cold_capacity = (cold_bytes_per_shard / 192).clamp(8_192, 1_000_000);
Self {
hot_capacity,
warm_capacity,
cold_capacity,
promotion_batch: 256,
}
}
}
impl Default for PersistenceConfig {
fn default() -> Self {
Self {
enabled: true,
data_dir: PathBuf::from("./var/fast-cache"),
segment_size_bytes: 64 * 1024 * 1024,
fsync_interval_ms: 100,
snapshot_every_seconds: 300,
snapshot_min_writes: 1_000,
compress_snapshots: true,
compress_wal: true,
wal_channel_capacity: 16_384,
tcp_export: WalTcpExportConfig::default(),
}
}
}
impl Default for WalTcpExportConfig {
fn default() -> Self {
Self {
enabled: false,
mode: WalTcpExportMode::Connect,
addr: "127.0.0.1:7630".to_string(),
auth_token: None,
channel_capacity: 16_384,
max_subscribers: 64,
connect_timeout_ms: 250,
write_timeout_ms: 250,
reconnect_backoff_ms: 100,
backpressure_on_full: false,
}
}
}
impl Default for ReplicationConfig {
fn default() -> Self {
Self {
enabled: false,
role: ReplicationRole::Primary,
bind_addr: "127.0.0.1:7631".to_string(),
replica_of: None,
auth_token: None,
compression: ReplicationCompression::None,
zstd_level: 3,
send_policy: ReplicationSendPolicy::Batch,
batch_max_records: 512,
batch_max_bytes: 1024 * 1024,
batch_max_delay_us: 750,
backlog_bytes: 64 * 1024 * 1024,
snapshot_chunk_bytes: 1024 * 1024,
queue_capacity: 16_384,
max_replicas: 16,
connect_timeout_ms: 500,
write_timeout_ms: 500,
reconnect_backoff_ms: 200,
subscriber_channel_capacity: 1_024,
}
}
}
impl FastCacheConfig {
pub fn default_shard_count() -> usize {
DefaultShardCount::current()
}
pub fn load_from_path(path: &Path) -> Result<Self> {
let config = ConfigFile::new(path).load()?;
config.validate()?;
Ok(config)
}
pub fn store_to_path(&self, path: &Path) -> Result<()> {
ConfigFile::new(path).store(self)
}
pub fn ensure_paths(&self) -> Result<()> {
self.persistence.ensure_paths()
}
pub fn validate(&self) -> Result<()> {
ConfigValidator::new(self).validate()
}
pub fn ttl_sweep_interval(&self) -> Duration {
Duration::from_millis(self.ttl_sweep_interval_ms.max(1))
}
pub fn stats_interval(&self) -> Duration {
Duration::from_millis(self.stats_interval_ms.max(250))
}
pub fn per_shard_memory_limit_bytes(&self) -> Option<usize> {
match self.max_memory_bytes {
0 => None,
bytes => {
let shard_count = self.shard_count as u64;
Some(bytes.div_ceil(shard_count) as usize)
}
}
}
pub fn snapshot_interval(&self) -> Duration {
Duration::from_secs(self.persistence.snapshot_every_seconds.max(1))
}
}
impl CacheGeometry {
pub fn detect_current_host() -> Self {
CacheGeometryDetector::detect()
}
}
impl<'a> ConfigFile<'a> {
fn new(path: &'a Path) -> Self {
ConfigFile { path }
}
fn load(&self) -> Result<FastCacheConfig> {
let contents = std::fs::read_to_string(self.path)?;
Ok(toml::from_str(&contents)?)
}
fn store(&self, config: &FastCacheConfig) -> Result<()> {
self.ensure_parent()?;
let contents = toml::to_string_pretty(config)?;
std::fs::write(self.path, contents)?;
Ok(())
}
fn ensure_parent(&self) -> Result<()> {
match ConfigFileParent::from_path(self.path) {
ConfigFileParent::Present(parent) => {
std::fs::create_dir_all(parent)?;
Ok(())
}
ConfigFileParent::Missing => Ok(()),
}
}
}
impl<'a> ConfigFileParent<'a> {
fn from_path(path: &'a Path) -> Self {
match path.parent() {
Some(parent) => Self::Present(parent),
None => Self::Missing,
}
}
}
impl PersistenceConfig {
fn ensure_paths(&self) -> Result<()> {
match self.enabled {
true => {
std::fs::create_dir_all(&self.data_dir)?;
Ok(())
}
false => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use super::{FastCacheConfig, geometry::CacheSizeParser};
#[test]
fn parses_cache_sizes() {
assert_eq!(CacheSizeParser::parse("32K"), Some(32 * 1024));
assert_eq!(CacheSizeParser::parse("4M"), Some(4 * 1024 * 1024));
assert_eq!(CacheSizeParser::parse("65536"), Some(65_536));
}
#[test]
fn validates_power_of_two_shard_count() {
for shard_count in [0, 3, 10, 12] {
let config = FastCacheConfig {
shard_count,
..FastCacheConfig::default()
};
assert!(config.validate().is_err(), "{shard_count} should fail");
}
for shard_count in [1, 2, 4, 8, 16, 32] {
let config = FastCacheConfig {
shard_count,
..FastCacheConfig::default()
};
assert!(config.validate().is_ok(), "{shard_count} should pass");
}
}
#[test]
fn default_shard_count_is_power_of_two() {
let shard_count = FastCacheConfig::default_shard_count();
assert!(shard_count > 0);
assert!(shard_count.is_power_of_two());
}
}