crates_docs/tools/docs/cache/
ttl.rs1use std::time::Duration;
4
5const DEFAULT_JITTER_RATIO: f64 = 0.1;
17
18const MIN_JITTER_RATIO: f64 = 0.0;
22
23const MAX_JITTER_RATIO: f64 = 1.0;
27
28const DEFAULT_CRATE_DOCS_TTL_SECS: u64 = 3600;
40
41const DEFAULT_SEARCH_RESULTS_TTL_SECS: u64 = 300;
53
54const DEFAULT_ITEM_DOCS_TTL_SECS: u64 = 1800;
66
67#[derive(Debug, Clone, Copy)]
78pub struct DocCacheTtl {
79 pub crate_docs_secs: u64,
81 pub search_results_secs: u64,
83 pub item_docs_secs: u64,
85 jitter_ratio: f64,
92}
93
94impl Default for DocCacheTtl {
95 fn default() -> Self {
96 Self {
97 crate_docs_secs: DEFAULT_CRATE_DOCS_TTL_SECS,
98 search_results_secs: DEFAULT_SEARCH_RESULTS_TTL_SECS,
99 item_docs_secs: DEFAULT_ITEM_DOCS_TTL_SECS,
100 jitter_ratio: DEFAULT_JITTER_RATIO,
101 }
102 }
103}
104
105impl DocCacheTtl {
106 #[must_use]
116 pub fn from_cache_config(config: &crate::cache::CacheConfig) -> Self {
117 Self {
118 crate_docs_secs: config
119 .crate_docs_ttl_secs
120 .unwrap_or(DEFAULT_CRATE_DOCS_TTL_SECS),
121 search_results_secs: config
122 .search_results_ttl_secs
123 .unwrap_or(DEFAULT_SEARCH_RESULTS_TTL_SECS),
124 item_docs_secs: config
125 .item_docs_ttl_secs
126 .unwrap_or(DEFAULT_ITEM_DOCS_TTL_SECS),
127 jitter_ratio: DEFAULT_JITTER_RATIO,
128 }
129 }
130
131 #[must_use]
144 pub fn with_jitter(
145 crate_docs_secs: u64,
146 search_results_secs: u64,
147 item_docs_secs: u64,
148 jitter_ratio: f64,
149 ) -> Self {
150 Self {
151 crate_docs_secs,
152 search_results_secs,
153 item_docs_secs,
154 jitter_ratio: Self::validate_jitter_ratio(jitter_ratio),
155 }
156 }
157
158 #[must_use]
171 fn validate_jitter_ratio(ratio: f64) -> f64 {
172 if ratio.is_nan() || ratio < MIN_JITTER_RATIO {
173 MIN_JITTER_RATIO
174 } else if ratio > MAX_JITTER_RATIO {
175 MAX_JITTER_RATIO
176 } else {
177 ratio
178 }
179 }
180
181 #[must_use]
183 pub const fn jitter_ratio(&self) -> f64 {
184 self.jitter_ratio
185 }
186
187 pub fn set_jitter_ratio(&mut self, ratio: f64) {
210 self.jitter_ratio = Self::validate_jitter_ratio(ratio);
211 }
212
213 #[must_use]
223 #[allow(clippy::cast_possible_truncation)]
224 #[allow(clippy::cast_sign_loss)]
225 #[allow(clippy::cast_precision_loss)]
226 pub fn apply_jitter(&self, base_ttl: u64) -> u64 {
227 let ratio = self.jitter_ratio.clamp(MIN_JITTER_RATIO, MAX_JITTER_RATIO);
229
230 if ratio <= MIN_JITTER_RATIO {
231 return base_ttl;
232 }
233
234 let rng = fastrand::f64();
235 let offset = (rng * 2.0 - 1.0) * ratio;
236
237 (base_ttl as f64 * (1.0 + offset)).max(1.0) as u64
238 }
239
240 #[must_use]
242 pub fn crate_docs_duration(&self) -> Duration {
243 Duration::from_secs(self.apply_jitter(self.crate_docs_secs))
244 }
245
246 #[must_use]
248 pub fn search_results_duration(&self) -> Duration {
249 Duration::from_secs(self.apply_jitter(self.search_results_secs))
250 }
251
252 #[must_use]
254 pub fn item_docs_duration(&self) -> Duration {
255 Duration::from_secs(self.apply_jitter(self.item_docs_secs))
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_doc_cache_ttl_default() {
265 let ttl = DocCacheTtl::default();
266 assert_eq!(ttl.crate_docs_secs, DEFAULT_CRATE_DOCS_TTL_SECS);
267 assert_eq!(ttl.search_results_secs, DEFAULT_SEARCH_RESULTS_TTL_SECS);
268 assert_eq!(ttl.item_docs_secs, DEFAULT_ITEM_DOCS_TTL_SECS);
269 assert!((ttl.jitter_ratio() - DEFAULT_JITTER_RATIO).abs() < f64::EPSILON);
270 }
271
272 #[test]
273 fn test_doc_cache_ttl_from_config() {
274 let config = crate::cache::CacheConfig {
275 cache_type: "memory".to_string(),
276 memory_size: Some(1000),
277 redis_url: None,
278 key_prefix: String::new(),
279 default_ttl: Some(DEFAULT_CRATE_DOCS_TTL_SECS),
280 crate_docs_ttl_secs: Some(7200),
281 item_docs_ttl_secs: Some(DEFAULT_CRATE_DOCS_TTL_SECS),
282 search_results_ttl_secs: Some(600),
283 };
284 let ttl = DocCacheTtl::from_cache_config(&config);
285 assert_eq!(ttl.crate_docs_secs, 7200);
286 assert_eq!(ttl.item_docs_secs, DEFAULT_CRATE_DOCS_TTL_SECS);
287 assert_eq!(ttl.search_results_secs, 600);
288 }
289
290 #[test]
291 fn test_apply_jitter_no_jitter() {
292 let mut ttl = DocCacheTtl::default();
293 ttl.set_jitter_ratio(0.0);
294 assert_eq!(ttl.apply_jitter(1000), 1000);
295 }
296
297 #[test]
298 fn test_apply_jitter_with_jitter() {
299 let mut ttl = DocCacheTtl::default();
300 ttl.set_jitter_ratio(0.5);
301
302 for _ in 0..100 {
303 let jittered = ttl.apply_jitter(1000);
304 assert!((500..=1500).contains(&jittered));
305 }
306 }
307
308 #[test]
309 fn test_durations() {
310 let mut ttl = DocCacheTtl::default();
311 ttl.set_jitter_ratio(0.0);
312 ttl.crate_docs_secs = DEFAULT_CRATE_DOCS_TTL_SECS;
313 ttl.search_results_secs = DEFAULT_SEARCH_RESULTS_TTL_SECS;
314 ttl.item_docs_secs = DEFAULT_ITEM_DOCS_TTL_SECS;
315
316 assert_eq!(
317 ttl.crate_docs_duration(),
318 Duration::from_secs(DEFAULT_CRATE_DOCS_TTL_SECS)
319 );
320 assert_eq!(
321 ttl.search_results_duration(),
322 Duration::from_secs(DEFAULT_SEARCH_RESULTS_TTL_SECS)
323 );
324 assert_eq!(
325 ttl.item_docs_duration(),
326 Duration::from_secs(DEFAULT_ITEM_DOCS_TTL_SECS)
327 );
328 }
329
330 #[test]
331 fn test_jitter_ratio_setter_validation() {
332 let mut ttl = DocCacheTtl::default();
333
334 ttl.set_jitter_ratio(0.5);
336 assert!((ttl.jitter_ratio() - 0.5).abs() < f64::EPSILON);
337
338 ttl.set_jitter_ratio(0.0);
340 assert!((ttl.jitter_ratio()).abs() < f64::EPSILON);
341
342 ttl.set_jitter_ratio(1.0);
343 assert!((ttl.jitter_ratio() - 1.0).abs() < f64::EPSILON);
344 }
345
346 #[test]
347 fn test_jitter_ratio_clamping() {
348 let mut ttl = DocCacheTtl::default();
349
350 ttl.set_jitter_ratio(1.5);
352 assert!((ttl.jitter_ratio() - 1.0).abs() < f64::EPSILON);
353
354 ttl.set_jitter_ratio(100.0);
355 assert!((ttl.jitter_ratio() - 1.0).abs() < f64::EPSILON);
356
357 ttl.set_jitter_ratio(-0.1);
359 assert!(ttl.jitter_ratio().abs() < f64::EPSILON);
360
361 ttl.set_jitter_ratio(-100.0);
362 assert!(ttl.jitter_ratio().abs() < f64::EPSILON);
363 }
364
365 #[test]
366 fn test_jitter_ratio_nan_handling() {
367 let mut ttl = DocCacheTtl::default();
368
369 ttl.set_jitter_ratio(f64::NAN);
371 assert!(ttl.jitter_ratio().abs() < f64::EPSILON);
372 }
373
374 #[test]
375 fn test_jitter_ratio_infinity_handling() {
376 let mut ttl = DocCacheTtl::default();
377
378 ttl.set_jitter_ratio(f64::INFINITY);
380 assert!((ttl.jitter_ratio() - 1.0).abs() < f64::EPSILON);
381
382 ttl.set_jitter_ratio(f64::NEG_INFINITY);
384 assert!(ttl.jitter_ratio().abs() < f64::EPSILON);
385 }
386
387 #[test]
388 fn test_apply_jitter_with_extreme_values() {
389 let mut ttl = DocCacheTtl::default();
391 ttl.set_jitter_ratio(0.0);
392 assert_eq!(ttl.apply_jitter(1000), 1000);
393
394 ttl.set_jitter_ratio(1.0);
396 for _ in 0..100 {
397 let jittered = ttl.apply_jitter(1000);
398 assert!((0..=2000).contains(&jittered));
399 }
400 }
401}