1mod key;
24mod stats;
25mod ttl;
26
27use crate::cache::Cache;
28use std::sync::Arc;
29
30pub use key::CacheKeyGenerator;
32pub use stats::CacheStats;
33pub use ttl::DocCacheTtl;
34
35#[derive(Clone)]
45pub struct DocCache {
46 cache: Arc<dyn Cache>,
47 ttl: DocCacheTtl,
48 stats: CacheStats,
49}
50
51impl DocCache {
52 pub fn new(cache: Arc<dyn Cache>) -> Self {
69 Self {
70 cache,
71 ttl: DocCacheTtl::default(),
72 stats: CacheStats::new(),
73 }
74 }
75
76 pub fn with_ttl(cache: Arc<dyn Cache>, ttl: DocCacheTtl) -> Self {
100 Self {
101 cache,
102 ttl,
103 stats: CacheStats::new(),
104 }
105 }
106
107 #[tracing::instrument(skip(self), fields(crate = crate_name, version = version), level = "trace")]
118 pub async fn get_crate_docs(
119 &self,
120 crate_name: &str,
121 version: Option<&str>,
122 ) -> Option<Arc<str>> {
123 let key = CacheKeyGenerator::crate_cache_key(crate_name, version);
124 let result = self.cache.get(&key).await;
125 let is_hit = result.is_some();
126 if is_hit {
127 self.stats.record_hit();
128 tracing::span!(
129 tracing::Level::TRACE,
130 "cache",
131 op = "get",
132 hit = true,
133 crate = crate_name
134 )
135 .in_scope(|| {
136 tracing::trace!("Cache hit for crate docs");
137 });
138 } else {
139 self.stats.record_miss();
140 tracing::span!(
141 tracing::Level::TRACE,
142 "cache",
143 op = "get",
144 hit = false,
145 crate = crate_name
146 )
147 .in_scope(|| {
148 tracing::trace!("Cache miss for crate docs");
149 });
150 }
151 result
152 }
153
154 #[tracing::instrument(skip(self, content), fields(crate = crate_name, version = version), err, level = "trace")]
166 pub async fn set_crate_docs(
167 &self,
168 crate_name: &str,
169 version: Option<&str>,
170 content: String,
171 ) -> crate::error::Result<()> {
172 let key = CacheKeyGenerator::crate_cache_key(crate_name, version);
173 let ttl = self.ttl.crate_docs_duration();
174 self.cache.set(key, content, Some(ttl)).await?;
175 self.stats.record_set();
176 tracing::trace!(ttl_secs = ttl.as_secs(), "Crate docs cached");
177 Ok(())
178 }
179
180 #[tracing::instrument(skip(self), fields(crate = crate_name, version = version), level = "trace")]
185 pub async fn get_crate_html(
186 &self,
187 crate_name: &str,
188 version: Option<&str>,
189 ) -> Option<Arc<str>> {
190 let key = CacheKeyGenerator::crate_html_cache_key(crate_name, version);
191 let result = self.cache.get(&key).await;
192 let is_hit = result.is_some();
193 if is_hit {
194 self.stats.record_hit();
195 tracing::span!(
196 tracing::Level::TRACE,
197 "cache",
198 op = "get_html",
199 hit = true,
200 crate = crate_name
201 )
202 .in_scope(|| {
203 tracing::trace!("Cache hit for crate HTML");
204 });
205 } else {
206 self.stats.record_miss();
207 tracing::span!(
208 tracing::Level::TRACE,
209 "cache",
210 op = "get_html",
211 hit = false,
212 crate = crate_name
213 )
214 .in_scope(|| {
215 tracing::trace!("Cache miss for crate HTML");
216 });
217 }
218 result
219 }
220
221 #[tracing::instrument(skip(self, content), fields(crate = crate_name, version = version), err, level = "trace")]
227 pub async fn set_crate_html(
228 &self,
229 crate_name: &str,
230 version: Option<&str>,
231 content: String,
232 ) -> crate::error::Result<()> {
233 let key = CacheKeyGenerator::crate_html_cache_key(crate_name, version);
234 let ttl = self.ttl.crate_docs_duration();
235 self.cache.set(key, content, Some(ttl)).await?;
236 self.stats.record_set();
237 tracing::trace!(ttl_secs = ttl.as_secs(), "Crate HTML cached");
238 Ok(())
239 }
240
241 #[tracing::instrument(skip(self), fields(query, limit, sort), level = "trace")]
253 pub async fn get_search_results(
254 &self,
255 query: &str,
256 limit: u32,
257 sort: Option<&str>,
258 ) -> Option<Arc<str>> {
259 let key = CacheKeyGenerator::search_cache_key(query, limit, sort);
260 let result = self.cache.get(&key).await;
261 let is_hit = result.is_some();
262 if is_hit {
263 self.stats.record_hit();
264 tracing::span!(
265 tracing::Level::TRACE,
266 "cache",
267 op = "get_search",
268 hit = true
269 )
270 .in_scope(|| {
271 tracing::trace!("Cache hit for search results");
272 });
273 } else {
274 self.stats.record_miss();
275 tracing::span!(
276 tracing::Level::TRACE,
277 "cache",
278 op = "get_search",
279 hit = false
280 )
281 .in_scope(|| {
282 tracing::trace!("Cache miss for search results");
283 });
284 }
285 result
286 }
287
288 #[tracing::instrument(skip(self, content), fields(query, limit, sort), err, level = "trace")]
301 pub async fn set_search_results(
302 &self,
303 query: &str,
304 limit: u32,
305 sort: Option<&str>,
306 content: String,
307 ) -> crate::error::Result<()> {
308 let key = CacheKeyGenerator::search_cache_key(query, limit, sort);
309 let ttl = self.ttl.search_results_duration();
310 self.cache.set(key, content, Some(ttl)).await?;
311 self.stats.record_set();
312 tracing::trace!(ttl_secs = ttl.as_secs(), "Search results cached");
313 Ok(())
314 }
315
316 #[tracing::instrument(skip(self), fields(crate = crate_name, item = item_path, version), level = "trace")]
328 pub async fn get_item_docs(
329 &self,
330 crate_name: &str,
331 item_path: &str,
332 version: Option<&str>,
333 ) -> Option<Arc<str>> {
334 let key = CacheKeyGenerator::item_cache_key(crate_name, item_path, version);
335 let result = self.cache.get(&key).await;
336 let is_hit = result.is_some();
337 if is_hit {
338 self.stats.record_hit();
339 tracing::span!(tracing::Level::TRACE, "cache", op = "get_item", hit = true).in_scope(
340 || {
341 tracing::trace!("Cache hit for item docs");
342 },
343 );
344 } else {
345 self.stats.record_miss();
346 tracing::span!(tracing::Level::TRACE, "cache", op = "get_item", hit = false).in_scope(
347 || {
348 tracing::trace!("Cache miss for item docs");
349 },
350 );
351 }
352 result
353 }
354
355 #[tracing::instrument(skip(self, content), fields(crate = crate_name, item = item_path, version), err, level = "trace")]
368 pub async fn set_item_docs(
369 &self,
370 crate_name: &str,
371 item_path: &str,
372 version: Option<&str>,
373 content: String,
374 ) -> crate::error::Result<()> {
375 let key = CacheKeyGenerator::item_cache_key(crate_name, item_path, version);
376 let ttl = self.ttl.item_docs_duration();
377 self.cache.set(key, content, Some(ttl)).await?;
378 self.stats.record_set();
379 tracing::trace!(ttl_secs = ttl.as_secs(), "Item docs cached");
380 Ok(())
381 }
382
383 #[tracing::instrument(skip(self), fields(crate = crate_name, item = item_path, version), level = "trace")]
388 pub async fn get_item_html(
389 &self,
390 crate_name: &str,
391 item_path: &str,
392 version: Option<&str>,
393 ) -> Option<Arc<str>> {
394 let key = CacheKeyGenerator::item_html_cache_key(crate_name, item_path, version);
395 let result = self.cache.get(&key).await;
396 let is_hit = result.is_some();
397 if is_hit {
398 self.stats.record_hit();
399 tracing::span!(
400 tracing::Level::TRACE,
401 "cache",
402 op = "get_item_html",
403 hit = true
404 )
405 .in_scope(|| {
406 tracing::trace!("Cache hit for item HTML");
407 });
408 } else {
409 self.stats.record_miss();
410 tracing::span!(
411 tracing::Level::TRACE,
412 "cache",
413 op = "get_item_html",
414 hit = false
415 )
416 .in_scope(|| {
417 tracing::trace!("Cache miss for item HTML");
418 });
419 }
420 result
421 }
422
423 #[tracing::instrument(skip(self, content), fields(crate = crate_name, item = item_path, version), err, level = "trace")]
429 pub async fn set_item_html(
430 &self,
431 crate_name: &str,
432 item_path: &str,
433 version: Option<&str>,
434 content: String,
435 ) -> crate::error::Result<()> {
436 let key = CacheKeyGenerator::item_html_cache_key(crate_name, item_path, version);
437 let ttl = self.ttl.item_docs_duration();
438 self.cache.set(key, content, Some(ttl)).await?;
439 self.stats.record_set();
440 tracing::trace!(ttl_secs = ttl.as_secs(), "Item HTML cached");
441 Ok(())
442 }
443
444 #[tracing::instrument(skip(self), err, level = "trace")]
450 pub async fn clear(&self) -> crate::error::Result<()> {
451 tracing::trace!("Clearing all doc cache entries");
452 self.cache.clear().await
453 }
454
455 #[must_use]
457 pub fn stats(&self) -> &CacheStats {
458 &self.stats
459 }
460
461 #[must_use]
463 pub fn ttl(&self) -> &DocCacheTtl {
464 &self.ttl
465 }
466}
467
468impl Default for DocCache {
469 fn default() -> Self {
470 let cache = Arc::new(crate::cache::memory::MemoryCache::new(1000));
471 Self::new(cache)
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use crate::cache::memory::MemoryCache;
479
480 #[tokio::test]
481 async fn test_doc_cache() {
482 let memory_cache = MemoryCache::new(100);
483 let cache = Arc::new(memory_cache);
484 let doc_cache = DocCache::new(cache);
485
486 doc_cache
488 .set_crate_docs("serde", Some("1.0"), "Test docs".to_string())
489 .await
490 .expect("set_crate_docs should succeed");
491 let cached = doc_cache.get_crate_docs("serde", Some("1.0")).await;
492 assert_eq!(
493 cached.as_ref().map(std::convert::AsRef::as_ref),
494 Some("Test docs")
495 );
496
497 doc_cache
499 .set_search_results(
500 "web framework",
501 10,
502 Some("relevance"),
503 "Search results".to_string(),
504 )
505 .await
506 .expect("set_search_results should succeed");
507 let search_cached = doc_cache
508 .get_search_results("web framework", 10, Some("relevance"))
509 .await;
510 assert_eq!(
511 search_cached.as_ref().map(std::convert::AsRef::as_ref),
512 Some("Search results")
513 );
514
515 doc_cache
517 .set_item_docs(
518 "serde",
519 "serde::Serialize",
520 Some("1.0"),
521 "Item docs".to_string(),
522 )
523 .await
524 .expect("set_item_docs should succeed");
525 let item_cached = doc_cache
526 .get_item_docs("serde", "serde::Serialize", Some("1.0"))
527 .await;
528 assert_eq!(
529 item_cached.as_ref().map(std::convert::AsRef::as_ref),
530 Some("Item docs")
531 );
532
533 doc_cache.clear().await.expect("clear should succeed");
535 let cleared = doc_cache.get_crate_docs("serde", Some("1.0")).await;
536 assert_eq!(cleared, None);
537 }
538
539 #[tokio::test]
540 async fn test_doc_cache_getters_preserve_shared_ownership() {
541 let memory_cache = Arc::new(MemoryCache::new(100));
542 let doc_cache = DocCache::new(memory_cache.clone());
543
544 doc_cache
545 .set_crate_docs("serde", Some("1.0"), "Test docs".to_string())
546 .await
547 .expect("set_crate_docs should succeed");
548
549 let key = CacheKeyGenerator::crate_cache_key("serde", Some("1.0"));
550 let cached_from_doc_cache = doc_cache
551 .get_crate_docs("serde", Some("1.0"))
552 .await
553 .expect("doc cache should return cached docs");
554 let cached_from_backend = memory_cache
555 .get(&key)
556 .await
557 .expect("backend cache should return cached docs");
558
559 assert!(Arc::ptr_eq(&cached_from_doc_cache, &cached_from_backend));
560 }
561
562 #[tokio::test]
563 async fn test_doc_cache_with_ttl() {
564 let memory_cache = MemoryCache::new(100);
565 let cache = Arc::new(memory_cache);
566
567 let mut ttl = DocCacheTtl::default();
568 ttl.crate_docs_secs = 7200;
569 ttl.search_results_secs = 600;
570 ttl.item_docs_secs = 3600;
571 ttl.set_jitter_ratio(0.0); let doc_cache = DocCache::with_ttl(cache, ttl);
574
575 assert_eq!(doc_cache.ttl().crate_docs_secs, 7200);
576 assert_eq!(doc_cache.ttl().search_results_secs, 600);
577 assert_eq!(doc_cache.ttl().item_docs_secs, 3600);
578 }
579
580 #[tokio::test]
581 async fn test_doc_cache_stats() {
582 let memory_cache = MemoryCache::new(100);
583 let cache = Arc::new(memory_cache);
584 let doc_cache = DocCache::new(cache);
585
586 doc_cache
588 .set_crate_docs("serde", None, "docs".to_string())
589 .await
590 .ok();
591 doc_cache.get_crate_docs("serde", None).await;
592
593 doc_cache.get_crate_docs("nonexistent", None).await;
595
596 assert_eq!(doc_cache.stats().hits(), 1);
597 assert_eq!(doc_cache.stats().misses(), 1);
598 assert_eq!(doc_cache.stats().sets(), 1);
599 }
600
601 #[test]
602 fn test_doc_cache_default() {
603 let doc_cache = DocCache::default();
604 assert_eq!(doc_cache.ttl().crate_docs_secs, 3600);
605 assert_eq!(doc_cache.ttl().search_results_secs, 300);
606 assert_eq!(doc_cache.ttl().item_docs_secs, 1800);
607 }
608}