1use crate::cache::Cache;
23use std::sync::Arc;
24use std::time::Duration;
25
26#[derive(Debug, Clone, Copy)]
37pub struct DocCacheTtl {
38 pub crate_docs_secs: u64,
40 pub search_results_secs: u64,
42 pub item_docs_secs: u64,
44 pub jitter_ratio: f64,
49}
50
51const DEFAULT_JITTER_RATIO: f64 = 0.1;
53
54impl Default for DocCacheTtl {
55 fn default() -> Self {
56 Self {
57 crate_docs_secs: 3600, search_results_secs: 300, item_docs_secs: 1800, jitter_ratio: DEFAULT_JITTER_RATIO,
61 }
62 }
63}
64
65impl DocCacheTtl {
66 #[must_use]
76 pub fn from_cache_config(config: &crate::cache::CacheConfig) -> Self {
77 Self {
78 crate_docs_secs: config.crate_docs_ttl_secs.unwrap_or(3600),
79 search_results_secs: config.search_results_ttl_secs.unwrap_or(300),
80 item_docs_secs: config.item_docs_ttl_secs.unwrap_or(1800),
81 jitter_ratio: DEFAULT_JITTER_RATIO,
82 }
83 }
84
85 #[must_use]
95 #[allow(clippy::cast_possible_truncation)]
96 #[allow(clippy::cast_sign_loss)]
97 #[allow(clippy::cast_precision_loss)]
98 pub fn apply_jitter(&self, base_ttl: u64) -> u64 {
99 if self.jitter_ratio <= 0.0 {
100 return base_ttl;
101 }
102
103 let ratio = self.jitter_ratio.clamp(0.0, 1.0);
105
106 let rng = fastrand::f64();
108 let offset = (rng * 2.0 - 1.0) * ratio;
109
110 (base_ttl as f64 * (1.0 + offset)).max(1.0) as u64
112 }
113}
114
115#[derive(Clone)]
124pub struct DocCache {
125 cache: Arc<dyn Cache>,
126 ttl: DocCacheTtl,
127}
128
129impl DocCache {
130 pub fn new(cache: Arc<dyn Cache>) -> Self {
147 Self {
148 cache,
149 ttl: DocCacheTtl::default(),
150 }
151 }
152
153 #[must_use]
177 pub fn with_ttl(cache: Arc<dyn Cache>, ttl: DocCacheTtl) -> Self {
178 Self { cache, ttl }
179 }
180
181 pub async fn get_crate_docs(&self, crate_name: &str, version: Option<&str>) -> Option<String> {
192 let key = Self::crate_cache_key(crate_name, version);
193 self.cache.get(&key).await
194 }
195
196 pub async fn set_crate_docs(
208 &self,
209 crate_name: &str,
210 version: Option<&str>,
211 content: String,
212 ) -> crate::error::Result<()> {
213 let key = Self::crate_cache_key(crate_name, version);
214 let ttl = Duration::from_secs(self.ttl.apply_jitter(self.ttl.crate_docs_secs));
215 self.cache.set(key, content, Some(ttl)).await
216 }
217
218 pub async fn get_search_results(&self, query: &str, limit: u32) -> Option<String> {
229 let key = Self::search_cache_key(query, limit);
230 self.cache.get(&key).await
231 }
232
233 pub async fn set_search_results(
245 &self,
246 query: &str,
247 limit: u32,
248 content: String,
249 ) -> crate::error::Result<()> {
250 let key = Self::search_cache_key(query, limit);
251 let ttl = Duration::from_secs(self.ttl.apply_jitter(self.ttl.search_results_secs));
252 self.cache.set(key, content, Some(ttl)).await
253 }
254
255 pub async fn get_item_docs(
267 &self,
268 crate_name: &str,
269 item_path: &str,
270 version: Option<&str>,
271 ) -> Option<String> {
272 let key = Self::item_cache_key(crate_name, item_path, version);
273 self.cache.get(&key).await
274 }
275
276 pub async fn set_item_docs(
289 &self,
290 crate_name: &str,
291 item_path: &str,
292 version: Option<&str>,
293 content: String,
294 ) -> crate::error::Result<()> {
295 let key = Self::item_cache_key(crate_name, item_path, version);
296 let ttl = Duration::from_secs(self.ttl.apply_jitter(self.ttl.item_docs_secs));
297 self.cache.set(key, content, Some(ttl)).await
298 }
299
300 pub async fn clear(&self) -> crate::error::Result<()> {
306 self.cache.clear().await
307 }
308
309 fn crate_cache_key(crate_name: &str, version: Option<&str>) -> String {
311 if let Some(ver) = version {
312 format!("crate:{crate_name}:{ver}")
313 } else {
314 format!("crate:{crate_name}")
315 }
316 }
317
318 fn search_cache_key(query: &str, limit: u32) -> String {
320 format!("search:{query}:{limit}")
321 }
322
323 fn item_cache_key(crate_name: &str, item_path: &str, version: Option<&str>) -> String {
325 if let Some(ver) = version {
326 format!("item:{crate_name}:{ver}:{item_path}")
327 } else {
328 format!("item:{crate_name}:{item_path}")
329 }
330 }
331}
332
333impl Default for DocCache {
334 fn default() -> Self {
335 let cache = Arc::new(crate::cache::memory::MemoryCache::new(1000));
336 Self::new(cache)
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use crate::cache::memory::MemoryCache;
344
345 #[tokio::test]
346 async fn test_doc_cache() {
347 let memory_cache = MemoryCache::new(100);
348 let cache = Arc::new(memory_cache);
349 let doc_cache = DocCache::new(cache);
350
351 doc_cache
353 .set_crate_docs("serde", Some("1.0"), "Test docs".to_string())
354 .await
355 .expect("set_crate_docs should succeed");
356 let cached = doc_cache.get_crate_docs("serde", Some("1.0")).await;
357 assert_eq!(cached, Some("Test docs".to_string()));
358
359 doc_cache
361 .set_search_results("web framework", 10, "Search results".to_string())
362 .await
363 .expect("set_search_results should succeed");
364 let search_cached = doc_cache.get_search_results("web framework", 10).await;
365 assert_eq!(search_cached, Some("Search results".to_string()));
366
367 doc_cache
369 .set_item_docs(
370 "serde",
371 "serde::Serialize",
372 Some("1.0"),
373 "Item docs".to_string(),
374 )
375 .await
376 .expect("set_item_docs should succeed");
377 let item_cached = doc_cache
378 .get_item_docs("serde", "serde::Serialize", Some("1.0"))
379 .await;
380 assert_eq!(item_cached, Some("Item docs".to_string()));
381
382 doc_cache.clear().await.expect("clear should succeed");
384 let cleared = doc_cache.get_crate_docs("serde", Some("1.0")).await;
385 assert_eq!(cleared, None);
386 }
387
388 #[test]
389 fn test_cache_key_generation() {
390 assert_eq!(DocCache::crate_cache_key("serde", None), "crate:serde");
391 assert_eq!(
392 DocCache::crate_cache_key("serde", Some("1.0")),
393 "crate:serde:1.0"
394 );
395
396 assert_eq!(
397 DocCache::search_cache_key("web framework", 10),
398 "search:web framework:10"
399 );
400
401 assert_eq!(
402 DocCache::item_cache_key("serde", "Serialize", None),
403 "item:serde:Serialize"
404 );
405 assert_eq!(
406 DocCache::item_cache_key("serde", "Serialize", Some("1.0")),
407 "item:serde:1.0:Serialize"
408 );
409 }
410
411 #[test]
412 fn test_doc_cache_ttl_default() {
413 let ttl = DocCacheTtl::default();
414 assert_eq!(ttl.crate_docs_secs, 3600);
415 assert_eq!(ttl.search_results_secs, 300);
416 assert_eq!(ttl.item_docs_secs, 1800);
417 }
418
419 #[test]
420 fn test_doc_cache_ttl_from_config() {
421 let config = crate::cache::CacheConfig {
422 cache_type: "memory".to_string(),
423 memory_size: Some(1000),
424 redis_url: None,
425 key_prefix: String::new(),
426 default_ttl: Some(3600),
427 crate_docs_ttl_secs: Some(7200),
428 item_docs_ttl_secs: Some(3600),
429 search_results_ttl_secs: Some(600),
430 };
431 let ttl = DocCacheTtl::from_cache_config(&config);
432 assert_eq!(ttl.crate_docs_secs, 7200);
433 assert_eq!(ttl.item_docs_secs, 3600);
434 assert_eq!(ttl.search_results_secs, 600);
435 }
436}