use super::policy::CachePolicyKind;
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct CacheConfig {
max_bytes: u64,
enable_persistence: bool,
cache_root: PathBuf,
background_writer: bool,
policy_kind: CachePolicyKind,
policy_window_ratio: f32,
}
impl CacheConfig {
pub const DEFAULT_MAX_BYTES: u64 = 50 * 1024 * 1024;
pub const DEFAULT_CACHE_ROOT: &'static str = ".sqry-cache";
pub const DEFAULT_POLICY_WINDOW_RATIO: f32 = 0.20;
pub const MIN_POLICY_WINDOW_RATIO: f32 = 0.05;
pub const MAX_POLICY_WINDOW_RATIO: f32 = 0.95;
#[must_use]
pub fn new() -> Self {
Self {
max_bytes: Self::DEFAULT_MAX_BYTES,
enable_persistence: true,
cache_root: PathBuf::from(Self::DEFAULT_CACHE_ROOT),
background_writer: true,
policy_kind: CachePolicyKind::default(),
policy_window_ratio: Self::DEFAULT_POLICY_WINDOW_RATIO,
}
}
#[must_use]
pub fn from_env() -> Self {
let mut config = Self::new();
if let Ok(max_bytes_str) = std::env::var("SQRY_CACHE_MAX_BYTES")
&& let Ok(max_bytes) = max_bytes_str.parse::<u64>()
{
config = config.with_max_bytes(max_bytes);
}
if let Ok(disable_persist) = std::env::var("SQRY_CACHE_DISABLE_PERSIST")
&& (disable_persist == "1" || disable_persist.eq_ignore_ascii_case("true"))
{
config = config.with_persistence(false);
}
if let Ok(cache_root) = std::env::var("SQRY_CACHE_ROOT") {
config = config.with_cache_root(PathBuf::from(cache_root));
}
if let Ok(policy_str) = std::env::var("SQRY_CACHE_POLICY") {
if let Some(kind) = CachePolicyKind::parse(&policy_str) {
config = config.with_policy_kind(kind);
} else {
log::warn!(
"Invalid SQRY_CACHE_POLICY='{policy_str}' (expected lru|tiny_lfu|hybrid), falling back to LRU"
);
}
}
if let Ok(window_ratio_str) = std::env::var("SQRY_CACHE_POLICY_WINDOW")
&& let Ok(ratio) = window_ratio_str.parse::<f32>()
{
config = config.with_policy_window_ratio(ratio);
}
config
}
#[must_use]
pub fn with_max_bytes(mut self, max_bytes: u64) -> Self {
self.max_bytes = max_bytes;
self
}
#[must_use]
pub fn with_persistence(mut self, enable: bool) -> Self {
self.enable_persistence = enable;
self
}
#[must_use]
pub fn with_cache_root(mut self, cache_root: PathBuf) -> Self {
self.cache_root = cache_root;
self
}
#[must_use]
pub fn with_background_writer(mut self, enable: bool) -> Self {
self.background_writer = enable;
self
}
#[must_use]
pub fn with_policy_kind(mut self, kind: CachePolicyKind) -> Self {
self.policy_kind = kind;
self
}
#[must_use]
pub fn with_policy_window_ratio(mut self, ratio: f32) -> Self {
self.policy_window_ratio = Self::clamp_window_ratio(ratio);
self
}
fn clamp_window_ratio(ratio: f32) -> f32 {
if ratio.is_nan() || !ratio.is_finite() {
Self::DEFAULT_POLICY_WINDOW_RATIO
} else {
ratio.clamp(Self::MIN_POLICY_WINDOW_RATIO, Self::MAX_POLICY_WINDOW_RATIO)
}
}
#[must_use]
pub fn max_bytes(&self) -> u64 {
self.max_bytes
}
#[must_use]
pub fn is_persistence_enabled(&self) -> bool {
self.enable_persistence
}
#[must_use]
pub fn cache_root(&self) -> &PathBuf {
&self.cache_root
}
#[must_use]
pub fn is_background_writer_enabled(&self) -> bool {
self.background_writer
}
#[must_use]
pub fn policy_kind(&self) -> CachePolicyKind {
self.policy_kind
}
#[must_use]
pub fn policy_window_ratio(&self) -> f32 {
self.policy_window_ratio
}
}
impl Default for CacheConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CachePolicy {
Enabled {
ttl: Duration,
},
Disabled,
}
impl CachePolicy {
pub const DEFAULT_TTL: Duration = Duration::from_secs(3600);
#[must_use]
pub fn default_enabled() -> Self {
Self::Enabled {
ttl: Self::DEFAULT_TTL,
}
}
#[must_use]
pub fn enabled(ttl: Duration) -> Self {
Self::Enabled { ttl }
}
#[must_use]
pub fn disabled() -> Self {
Self::Disabled
}
#[must_use]
pub fn is_enabled(&self) -> bool {
matches!(self, Self::Enabled { .. })
}
#[must_use]
pub fn ttl(&self) -> Option<Duration> {
match self {
Self::Enabled { ttl } => Some(*ttl),
Self::Disabled => None,
}
}
}
impl Default for CachePolicy {
fn default() -> Self {
Self::default_enabled()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_config_default() {
let config = CacheConfig::default();
assert_eq!(config.max_bytes(), CacheConfig::DEFAULT_MAX_BYTES);
assert!(config.is_persistence_enabled());
assert_eq!(config.cache_root(), &PathBuf::from(".sqry-cache"));
assert!(config.is_background_writer_enabled());
assert_eq!(config.policy_kind(), CachePolicyKind::Lru);
assert!(
(config.policy_window_ratio() - CacheConfig::DEFAULT_POLICY_WINDOW_RATIO).abs()
< f32::EPSILON
);
}
#[test]
fn test_cache_config_builder() {
let config = CacheConfig::new()
.with_max_bytes(100 * 1024 * 1024)
.with_persistence(false)
.with_cache_root(PathBuf::from("/tmp/cache"))
.with_background_writer(false)
.with_policy_kind(CachePolicyKind::TinyLfu)
.with_policy_window_ratio(0.5);
assert_eq!(config.max_bytes(), 100 * 1024 * 1024);
assert!(!config.is_persistence_enabled());
assert_eq!(config.cache_root(), &PathBuf::from("/tmp/cache"));
assert!(!config.is_background_writer_enabled());
assert_eq!(config.policy_kind(), CachePolicyKind::TinyLfu);
assert!((config.policy_window_ratio() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_cache_config_from_env() {
unsafe {
std::env::set_var("SQRY_CACHE_MAX_BYTES", "104857600"); std::env::set_var("SQRY_CACHE_DISABLE_PERSIST", "1");
std::env::set_var("SQRY_CACHE_ROOT", "/tmp/test-cache");
std::env::set_var("SQRY_CACHE_POLICY", "tiny_lfu");
std::env::set_var("SQRY_CACHE_POLICY_WINDOW", "0.33");
}
let config = CacheConfig::from_env();
assert_eq!(config.max_bytes(), 104_857_600);
assert!(!config.is_persistence_enabled());
assert_eq!(config.cache_root(), &PathBuf::from("/tmp/test-cache"));
assert_eq!(config.policy_kind(), CachePolicyKind::TinyLfu);
assert!((config.policy_window_ratio() - 0.33).abs() < f32::EPSILON);
unsafe {
std::env::remove_var("SQRY_CACHE_MAX_BYTES");
std::env::remove_var("SQRY_CACHE_DISABLE_PERSIST");
std::env::remove_var("SQRY_CACHE_ROOT");
std::env::remove_var("SQRY_CACHE_POLICY");
std::env::remove_var("SQRY_CACHE_POLICY_WINDOW");
}
}
#[test]
fn test_policy_window_ratio_clamp() {
let config = CacheConfig::new()
.with_policy_window_ratio(0.99)
.with_policy_window_ratio(0.01)
.with_policy_window_ratio(f32::NAN);
assert!(
(config.policy_window_ratio() - CacheConfig::DEFAULT_POLICY_WINDOW_RATIO).abs()
< f32::EPSILON
);
}
#[test]
fn test_cache_policy_default() {
let policy = CachePolicy::default();
assert!(policy.is_enabled());
assert_eq!(policy.ttl(), Some(Duration::from_secs(3600)));
}
#[test]
fn test_cache_policy_enabled() {
let policy = CachePolicy::enabled(Duration::from_secs(7200));
assert!(policy.is_enabled());
assert_eq!(policy.ttl(), Some(Duration::from_secs(7200)));
}
#[test]
fn test_cache_policy_disabled() {
let policy = CachePolicy::disabled();
assert!(!policy.is_enabled());
assert_eq!(policy.ttl(), None);
}
#[test]
fn test_cache_policy_equality() {
let policy1 = CachePolicy::enabled(Duration::from_secs(3600));
let policy2 = CachePolicy::enabled(Duration::from_secs(3600));
let policy3 = CachePolicy::enabled(Duration::from_secs(7200));
let policy4 = CachePolicy::disabled();
assert_eq!(policy1, policy2);
assert_ne!(policy1, policy3);
assert_ne!(policy1, policy4);
}
}