use std::time::Duration;
const DEFAULT_JITTER_RATIO: f64 = 0.1;
const MIN_JITTER_RATIO: f64 = 0.0;
const MAX_JITTER_RATIO: f64 = 1.0;
const DEFAULT_CRATE_DOCS_TTL_SECS: u64 = 3600;
const DEFAULT_SEARCH_RESULTS_TTL_SECS: u64 = 300;
const DEFAULT_ITEM_DOCS_TTL_SECS: u64 = 1800;
#[derive(Debug, Clone, Copy)]
pub struct DocCacheTtl {
pub crate_docs_secs: u64,
pub search_results_secs: u64,
pub item_docs_secs: u64,
jitter_ratio: f64,
}
impl Default for DocCacheTtl {
fn default() -> Self {
Self {
crate_docs_secs: DEFAULT_CRATE_DOCS_TTL_SECS,
search_results_secs: DEFAULT_SEARCH_RESULTS_TTL_SECS,
item_docs_secs: DEFAULT_ITEM_DOCS_TTL_SECS,
jitter_ratio: DEFAULT_JITTER_RATIO,
}
}
}
impl DocCacheTtl {
#[must_use]
pub fn from_cache_config(config: &crate::cache::CacheConfig) -> Self {
Self {
crate_docs_secs: config
.crate_docs_ttl_secs
.unwrap_or(DEFAULT_CRATE_DOCS_TTL_SECS),
search_results_secs: config
.search_results_ttl_secs
.unwrap_or(DEFAULT_SEARCH_RESULTS_TTL_SECS),
item_docs_secs: config
.item_docs_ttl_secs
.unwrap_or(DEFAULT_ITEM_DOCS_TTL_SECS),
jitter_ratio: DEFAULT_JITTER_RATIO,
}
}
#[must_use]
pub fn with_jitter(
crate_docs_secs: u64,
search_results_secs: u64,
item_docs_secs: u64,
jitter_ratio: f64,
) -> Self {
Self {
crate_docs_secs,
search_results_secs,
item_docs_secs,
jitter_ratio: Self::validate_jitter_ratio(jitter_ratio),
}
}
#[must_use]
fn validate_jitter_ratio(ratio: f64) -> f64 {
if ratio.is_nan() || ratio < MIN_JITTER_RATIO {
MIN_JITTER_RATIO
} else if ratio > MAX_JITTER_RATIO {
MAX_JITTER_RATIO
} else {
ratio
}
}
#[must_use]
pub const fn jitter_ratio(&self) -> f64 {
self.jitter_ratio
}
pub fn set_jitter_ratio(&mut self, ratio: f64) {
self.jitter_ratio = Self::validate_jitter_ratio(ratio);
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_precision_loss)]
pub fn apply_jitter(&self, base_ttl: u64) -> u64 {
let ratio = self.jitter_ratio.clamp(MIN_JITTER_RATIO, MAX_JITTER_RATIO);
if ratio <= MIN_JITTER_RATIO {
return base_ttl;
}
let rng = fastrand::f64();
let offset = (rng * 2.0 - 1.0) * ratio;
(base_ttl as f64 * (1.0 + offset)).max(1.0) as u64
}
#[must_use]
pub fn crate_docs_duration(&self) -> Duration {
Duration::from_secs(self.apply_jitter(self.crate_docs_secs))
}
#[must_use]
pub fn search_results_duration(&self) -> Duration {
Duration::from_secs(self.apply_jitter(self.search_results_secs))
}
#[must_use]
pub fn item_docs_duration(&self) -> Duration {
Duration::from_secs(self.apply_jitter(self.item_docs_secs))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_doc_cache_ttl_default() {
let ttl = DocCacheTtl::default();
assert_eq!(ttl.crate_docs_secs, DEFAULT_CRATE_DOCS_TTL_SECS);
assert_eq!(ttl.search_results_secs, DEFAULT_SEARCH_RESULTS_TTL_SECS);
assert_eq!(ttl.item_docs_secs, DEFAULT_ITEM_DOCS_TTL_SECS);
assert!((ttl.jitter_ratio() - DEFAULT_JITTER_RATIO).abs() < f64::EPSILON);
}
#[test]
fn test_doc_cache_ttl_from_config() {
let config = crate::cache::CacheConfig {
cache_type: "memory".to_string(),
memory_size: Some(1000),
redis_url: None,
key_prefix: String::new(),
default_ttl: Some(DEFAULT_CRATE_DOCS_TTL_SECS),
crate_docs_ttl_secs: Some(7200),
item_docs_ttl_secs: Some(DEFAULT_CRATE_DOCS_TTL_SECS),
search_results_ttl_secs: Some(600),
};
let ttl = DocCacheTtl::from_cache_config(&config);
assert_eq!(ttl.crate_docs_secs, 7200);
assert_eq!(ttl.item_docs_secs, DEFAULT_CRATE_DOCS_TTL_SECS);
assert_eq!(ttl.search_results_secs, 600);
}
#[test]
fn test_apply_jitter_no_jitter() {
let mut ttl = DocCacheTtl::default();
ttl.set_jitter_ratio(0.0);
assert_eq!(ttl.apply_jitter(1000), 1000);
}
#[test]
fn test_apply_jitter_with_jitter() {
let mut ttl = DocCacheTtl::default();
ttl.set_jitter_ratio(0.5);
for _ in 0..100 {
let jittered = ttl.apply_jitter(1000);
assert!((500..=1500).contains(&jittered));
}
}
#[test]
fn test_durations() {
let mut ttl = DocCacheTtl::default();
ttl.set_jitter_ratio(0.0);
ttl.crate_docs_secs = DEFAULT_CRATE_DOCS_TTL_SECS;
ttl.search_results_secs = DEFAULT_SEARCH_RESULTS_TTL_SECS;
ttl.item_docs_secs = DEFAULT_ITEM_DOCS_TTL_SECS;
assert_eq!(
ttl.crate_docs_duration(),
Duration::from_secs(DEFAULT_CRATE_DOCS_TTL_SECS)
);
assert_eq!(
ttl.search_results_duration(),
Duration::from_secs(DEFAULT_SEARCH_RESULTS_TTL_SECS)
);
assert_eq!(
ttl.item_docs_duration(),
Duration::from_secs(DEFAULT_ITEM_DOCS_TTL_SECS)
);
}
#[test]
fn test_jitter_ratio_setter_validation() {
let mut ttl = DocCacheTtl::default();
ttl.set_jitter_ratio(0.5);
assert!((ttl.jitter_ratio() - 0.5).abs() < f64::EPSILON);
ttl.set_jitter_ratio(0.0);
assert!((ttl.jitter_ratio()).abs() < f64::EPSILON);
ttl.set_jitter_ratio(1.0);
assert!((ttl.jitter_ratio() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_jitter_ratio_clamping() {
let mut ttl = DocCacheTtl::default();
ttl.set_jitter_ratio(1.5);
assert!((ttl.jitter_ratio() - 1.0).abs() < f64::EPSILON);
ttl.set_jitter_ratio(100.0);
assert!((ttl.jitter_ratio() - 1.0).abs() < f64::EPSILON);
ttl.set_jitter_ratio(-0.1);
assert!(ttl.jitter_ratio().abs() < f64::EPSILON);
ttl.set_jitter_ratio(-100.0);
assert!(ttl.jitter_ratio().abs() < f64::EPSILON);
}
#[test]
fn test_jitter_ratio_nan_handling() {
let mut ttl = DocCacheTtl::default();
ttl.set_jitter_ratio(f64::NAN);
assert!(ttl.jitter_ratio().abs() < f64::EPSILON);
}
#[test]
fn test_jitter_ratio_infinity_handling() {
let mut ttl = DocCacheTtl::default();
ttl.set_jitter_ratio(f64::INFINITY);
assert!((ttl.jitter_ratio() - 1.0).abs() < f64::EPSILON);
ttl.set_jitter_ratio(f64::NEG_INFINITY);
assert!(ttl.jitter_ratio().abs() < f64::EPSILON);
}
#[test]
fn test_apply_jitter_with_extreme_values() {
let mut ttl = DocCacheTtl::default();
ttl.set_jitter_ratio(0.0);
assert_eq!(ttl.apply_jitter(1000), 1000);
ttl.set_jitter_ratio(1.0);
for _ in 0..100 {
let jittered = ttl.apply_jitter(1000);
assert!((0..=2000).contains(&jittered));
}
}
}