Skip to main content

crates_docs/tools/docs/cache/
ttl.rs

1//! TTL (Time-To-Live) management for document cache
2
3use std::time::Duration;
4
5/// Default TTL jitter ratio (10%)
6///
7/// # Value
8///
9/// 0.1 (10%)
10///
11/// # Rationale
12///
13/// A 10% jitter helps prevent cache stampede when multiple requests expire simultaneously.
14/// This spreads the load over time while maintaining reasonable cache consistency.
15/// Configurable via `DocCacheTtl::jitter_ratio` field.
16const DEFAULT_JITTER_RATIO: f64 = 0.1;
17
18/// Default crate documentation TTL in seconds
19///
20/// # Value
21///
22/// 3600 seconds (1 hour)
23///
24/// # Rationale
25///
26/// Crate documentation changes infrequently and is relatively large, making it suitable for longer caching.
27/// This reduces load on docs.rs while ensuring reasonable freshness.
28/// Configurable via `CacheConfig::crate_docs_ttl_secs`.
29const DEFAULT_CRATE_DOCS_TTL_SECS: u64 = 3600;
30
31/// Default search results TTL in seconds
32///
33/// # Value
34///
35/// 300 seconds (5 minutes)
36///
37/// # Rationale
38///
39/// Search results change frequently as new crates are published.
40/// Short TTL ensures users see recent additions while still benefiting from caching.
41/// Configurable via `CacheConfig::search_results_ttl_secs`.
42const DEFAULT_SEARCH_RESULTS_TTL_SECS: u64 = 300;
43
44/// Default item documentation TTL in seconds
45///
46/// # Value
47///
48/// 1800 seconds (30 minutes)
49///
50/// # Rationale
51///
52/// Item documentation (functions, structs) changes moderately often.
53/// Medium TTL balances freshness with performance.
54/// Configurable via `CacheConfig::item_docs_ttl_secs`.
55const DEFAULT_ITEM_DOCS_TTL_SECS: u64 = 1800;
56
57/// Document cache TTL configuration
58///
59/// Configure independent TTL for different document types.
60///
61/// # Fields
62///
63/// - `crate_docs_secs`: Crate document cache duration (seconds)
64/// - `search_results_secs`: search results cache duration (seconds)
65/// - `item_docs_secs`: item docs cache duration (seconds)
66/// - `jitter_ratio`: TTL jitter ratio(0.0-1.0),used to prevent cache stampede
67#[derive(Debug, Clone, Copy)]
68pub struct DocCacheTtl {
69    /// Crate document TTL (seconds)
70    pub crate_docs_secs: u64,
71    /// Search results TTL (seconds)
72    pub search_results_secs: u64,
73    /// Item documentation TTL (seconds)
74    pub item_docs_secs: u64,
75    /// TTL jitter ratio (0.0-1.0), default 0.1 (10%)
76    ///
77    /// Actual TTL = `base_ttl * (1 + random(-jitter_ratio, jitter_ratio))`
78    /// for example:`base_ttl=3600`, `jitter_ratio=0.1` => Actual TTL range `[3240, 3960]`
79    pub jitter_ratio: f64,
80}
81
82impl Default for DocCacheTtl {
83    fn default() -> Self {
84        Self {
85            crate_docs_secs: DEFAULT_CRATE_DOCS_TTL_SECS,
86            search_results_secs: DEFAULT_SEARCH_RESULTS_TTL_SECS,
87            item_docs_secs: DEFAULT_ITEM_DOCS_TTL_SECS,
88            jitter_ratio: DEFAULT_JITTER_RATIO,
89        }
90    }
91}
92
93impl DocCacheTtl {
94    /// Create TTL configuration from `CacheConfig`
95    ///
96    /// # Arguments
97    ///
98    /// * `config` - cache configuration
99    ///
100    /// # Returns
101    ///
102    /// Returns TTL configuration based on config
103    #[must_use]
104    pub fn from_cache_config(config: &crate::cache::CacheConfig) -> Self {
105        Self {
106            crate_docs_secs: config
107                .crate_docs_ttl_secs
108                .unwrap_or(DEFAULT_CRATE_DOCS_TTL_SECS),
109            search_results_secs: config
110                .search_results_ttl_secs
111                .unwrap_or(DEFAULT_SEARCH_RESULTS_TTL_SECS),
112            item_docs_secs: config
113                .item_docs_ttl_secs
114                .unwrap_or(DEFAULT_ITEM_DOCS_TTL_SECS),
115            jitter_ratio: DEFAULT_JITTER_RATIO,
116        }
117    }
118
119    /// Calculate actual TTL with jitter
120    ///
121    /// # Arguments
122    ///
123    /// * `base_ttl` - Base TTL (seconds)
124    ///
125    /// # Returns
126    ///
127    /// Returns jittered TTL (seconds)
128    #[must_use]
129    #[allow(clippy::cast_possible_truncation)]
130    #[allow(clippy::cast_sign_loss)]
131    #[allow(clippy::cast_precision_loss)]
132    pub fn apply_jitter(&self, base_ttl: u64) -> u64 {
133        if self.jitter_ratio <= 0.0 {
134            return base_ttl;
135        }
136
137        let ratio = self.jitter_ratio.clamp(0.0, 1.0);
138        let rng = fastrand::f64();
139        let offset = (rng * 2.0 - 1.0) * ratio;
140
141        (base_ttl as f64 * (1.0 + offset)).max(1.0) as u64
142    }
143
144    /// Get TTL duration for crate docs with jitter applied
145    #[must_use]
146    pub fn crate_docs_duration(&self) -> Duration {
147        Duration::from_secs(self.apply_jitter(self.crate_docs_secs))
148    }
149
150    /// Get TTL duration for search results with jitter applied
151    #[must_use]
152    pub fn search_results_duration(&self) -> Duration {
153        Duration::from_secs(self.apply_jitter(self.search_results_secs))
154    }
155
156    /// Get TTL duration for item docs with jitter applied
157    #[must_use]
158    pub fn item_docs_duration(&self) -> Duration {
159        Duration::from_secs(self.apply_jitter(self.item_docs_secs))
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_doc_cache_ttl_default() {
169        let ttl = DocCacheTtl::default();
170        assert_eq!(ttl.crate_docs_secs, DEFAULT_CRATE_DOCS_TTL_SECS);
171        assert_eq!(ttl.search_results_secs, DEFAULT_SEARCH_RESULTS_TTL_SECS);
172        assert_eq!(ttl.item_docs_secs, DEFAULT_ITEM_DOCS_TTL_SECS);
173        assert!((ttl.jitter_ratio - DEFAULT_JITTER_RATIO).abs() < f64::EPSILON);
174    }
175
176    #[test]
177    fn test_doc_cache_ttl_from_config() {
178        let config = crate::cache::CacheConfig {
179            cache_type: "memory".to_string(),
180            memory_size: Some(1000),
181            redis_url: None,
182            key_prefix: String::new(),
183            default_ttl: Some(DEFAULT_CRATE_DOCS_TTL_SECS),
184            crate_docs_ttl_secs: Some(7200),
185            item_docs_ttl_secs: Some(DEFAULT_CRATE_DOCS_TTL_SECS),
186            search_results_ttl_secs: Some(600),
187        };
188        let ttl = DocCacheTtl::from_cache_config(&config);
189        assert_eq!(ttl.crate_docs_secs, 7200);
190        assert_eq!(ttl.item_docs_secs, DEFAULT_CRATE_DOCS_TTL_SECS);
191        assert_eq!(ttl.search_results_secs, 600);
192    }
193
194    #[test]
195    fn test_apply_jitter_no_jitter() {
196        let ttl = DocCacheTtl {
197            jitter_ratio: 0.0,
198            ..Default::default()
199        };
200        assert_eq!(ttl.apply_jitter(1000), 1000);
201    }
202
203    #[test]
204    fn test_apply_jitter_with_jitter() {
205        let ttl = DocCacheTtl {
206            jitter_ratio: 0.5,
207            ..Default::default()
208        };
209
210        for _ in 0..100 {
211            let jittered = ttl.apply_jitter(1000);
212            assert!((500..=1500).contains(&jittered));
213        }
214    }
215
216    #[test]
217    fn test_durations() {
218        let ttl = DocCacheTtl {
219            jitter_ratio: 0.0,
220            crate_docs_secs: DEFAULT_CRATE_DOCS_TTL_SECS,
221            search_results_secs: DEFAULT_SEARCH_RESULTS_TTL_SECS,
222            item_docs_secs: DEFAULT_ITEM_DOCS_TTL_SECS,
223        };
224
225        assert_eq!(
226            ttl.crate_docs_duration(),
227            Duration::from_secs(DEFAULT_CRATE_DOCS_TTL_SECS)
228        );
229        assert_eq!(
230            ttl.search_results_duration(),
231            Duration::from_secs(DEFAULT_SEARCH_RESULTS_TTL_SECS)
232        );
233        assert_eq!(
234            ttl.item_docs_duration(),
235            Duration::from_secs(DEFAULT_ITEM_DOCS_TTL_SECS)
236        );
237    }
238}