mod key;
mod stats;
mod ttl;
use crate::cache::Cache;
use std::sync::Arc;
pub use key::CacheKeyGenerator;
pub use stats::CacheStats;
pub use ttl::DocCacheTtl;
#[derive(Clone)]
pub struct DocCache {
cache: Arc<dyn Cache>,
ttl: DocCacheTtl,
stats: CacheStats,
}
impl DocCache {
pub fn new(cache: Arc<dyn Cache>) -> Self {
Self {
cache,
ttl: DocCacheTtl::default(),
stats: CacheStats::new(),
}
}
pub fn with_ttl(cache: Arc<dyn Cache>, ttl: DocCacheTtl) -> Self {
Self {
cache,
ttl,
stats: CacheStats::new(),
}
}
#[tracing::instrument(skip(self), fields(crate = crate_name, version = version), level = "trace")]
pub async fn get_crate_docs(
&self,
crate_name: &str,
version: Option<&str>,
) -> Option<Arc<str>> {
let key = CacheKeyGenerator::crate_cache_key(crate_name, version);
let result = self.cache.get(&key).await;
let is_hit = result.is_some();
if is_hit {
self.stats.record_hit();
tracing::span!(
tracing::Level::TRACE,
"cache",
op = "get",
hit = true,
crate = crate_name
)
.in_scope(|| {
tracing::trace!("Cache hit for crate docs");
});
} else {
self.stats.record_miss();
tracing::span!(
tracing::Level::TRACE,
"cache",
op = "get",
hit = false,
crate = crate_name
)
.in_scope(|| {
tracing::trace!("Cache miss for crate docs");
});
}
result
}
#[tracing::instrument(skip(self, content), fields(crate = crate_name, version = version), err, level = "trace")]
pub async fn set_crate_docs(
&self,
crate_name: &str,
version: Option<&str>,
content: String,
) -> crate::error::Result<()> {
let key = CacheKeyGenerator::crate_cache_key(crate_name, version);
let ttl = self.ttl.crate_docs_duration();
self.cache.set(key, content, Some(ttl)).await?;
self.stats.record_set();
tracing::trace!(ttl_secs = ttl.as_secs(), "Crate docs cached");
Ok(())
}
#[tracing::instrument(skip(self), fields(crate = crate_name, version = version), level = "trace")]
pub async fn get_crate_html(
&self,
crate_name: &str,
version: Option<&str>,
) -> Option<Arc<str>> {
let key = CacheKeyGenerator::crate_html_cache_key(crate_name, version);
let result = self.cache.get(&key).await;
let is_hit = result.is_some();
if is_hit {
self.stats.record_hit();
tracing::span!(
tracing::Level::TRACE,
"cache",
op = "get_html",
hit = true,
crate = crate_name
)
.in_scope(|| {
tracing::trace!("Cache hit for crate HTML");
});
} else {
self.stats.record_miss();
tracing::span!(
tracing::Level::TRACE,
"cache",
op = "get_html",
hit = false,
crate = crate_name
)
.in_scope(|| {
tracing::trace!("Cache miss for crate HTML");
});
}
result
}
#[tracing::instrument(skip(self, content), fields(crate = crate_name, version = version), err, level = "trace")]
pub async fn set_crate_html(
&self,
crate_name: &str,
version: Option<&str>,
content: String,
) -> crate::error::Result<()> {
let key = CacheKeyGenerator::crate_html_cache_key(crate_name, version);
let ttl = self.ttl.crate_docs_duration();
self.cache.set(key, content, Some(ttl)).await?;
self.stats.record_set();
tracing::trace!(ttl_secs = ttl.as_secs(), "Crate HTML cached");
Ok(())
}
#[tracing::instrument(skip(self), fields(query, limit, sort), level = "trace")]
pub async fn get_search_results(
&self,
query: &str,
limit: u32,
sort: Option<&str>,
) -> Option<Arc<str>> {
let key = CacheKeyGenerator::search_cache_key(query, limit, sort);
let result = self.cache.get(&key).await;
let is_hit = result.is_some();
if is_hit {
self.stats.record_hit();
tracing::span!(
tracing::Level::TRACE,
"cache",
op = "get_search",
hit = true
)
.in_scope(|| {
tracing::trace!("Cache hit for search results");
});
} else {
self.stats.record_miss();
tracing::span!(
tracing::Level::TRACE,
"cache",
op = "get_search",
hit = false
)
.in_scope(|| {
tracing::trace!("Cache miss for search results");
});
}
result
}
#[tracing::instrument(skip(self, content), fields(query, limit, sort), err, level = "trace")]
pub async fn set_search_results(
&self,
query: &str,
limit: u32,
sort: Option<&str>,
content: String,
) -> crate::error::Result<()> {
let key = CacheKeyGenerator::search_cache_key(query, limit, sort);
let ttl = self.ttl.search_results_duration();
self.cache.set(key, content, Some(ttl)).await?;
self.stats.record_set();
tracing::trace!(ttl_secs = ttl.as_secs(), "Search results cached");
Ok(())
}
#[tracing::instrument(skip(self), fields(crate = crate_name, item = item_path, version), level = "trace")]
pub async fn get_item_docs(
&self,
crate_name: &str,
item_path: &str,
version: Option<&str>,
) -> Option<Arc<str>> {
let key = CacheKeyGenerator::item_cache_key(crate_name, item_path, version);
let result = self.cache.get(&key).await;
let is_hit = result.is_some();
if is_hit {
self.stats.record_hit();
tracing::span!(tracing::Level::TRACE, "cache", op = "get_item", hit = true).in_scope(
|| {
tracing::trace!("Cache hit for item docs");
},
);
} else {
self.stats.record_miss();
tracing::span!(tracing::Level::TRACE, "cache", op = "get_item", hit = false).in_scope(
|| {
tracing::trace!("Cache miss for item docs");
},
);
}
result
}
#[tracing::instrument(skip(self, content), fields(crate = crate_name, item = item_path, version), err, level = "trace")]
pub async fn set_item_docs(
&self,
crate_name: &str,
item_path: &str,
version: Option<&str>,
content: String,
) -> crate::error::Result<()> {
let key = CacheKeyGenerator::item_cache_key(crate_name, item_path, version);
let ttl = self.ttl.item_docs_duration();
self.cache.set(key, content, Some(ttl)).await?;
self.stats.record_set();
tracing::trace!(ttl_secs = ttl.as_secs(), "Item docs cached");
Ok(())
}
#[tracing::instrument(skip(self), fields(crate = crate_name, item = item_path, version), level = "trace")]
pub async fn get_item_html(
&self,
crate_name: &str,
item_path: &str,
version: Option<&str>,
) -> Option<Arc<str>> {
let key = CacheKeyGenerator::item_html_cache_key(crate_name, item_path, version);
let result = self.cache.get(&key).await;
let is_hit = result.is_some();
if is_hit {
self.stats.record_hit();
tracing::span!(
tracing::Level::TRACE,
"cache",
op = "get_item_html",
hit = true
)
.in_scope(|| {
tracing::trace!("Cache hit for item HTML");
});
} else {
self.stats.record_miss();
tracing::span!(
tracing::Level::TRACE,
"cache",
op = "get_item_html",
hit = false
)
.in_scope(|| {
tracing::trace!("Cache miss for item HTML");
});
}
result
}
#[tracing::instrument(skip(self, content), fields(crate = crate_name, item = item_path, version), err, level = "trace")]
pub async fn set_item_html(
&self,
crate_name: &str,
item_path: &str,
version: Option<&str>,
content: String,
) -> crate::error::Result<()> {
let key = CacheKeyGenerator::item_html_cache_key(crate_name, item_path, version);
let ttl = self.ttl.item_docs_duration();
self.cache.set(key, content, Some(ttl)).await?;
self.stats.record_set();
tracing::trace!(ttl_secs = ttl.as_secs(), "Item HTML cached");
Ok(())
}
#[tracing::instrument(skip(self), err, level = "trace")]
pub async fn clear(&self) -> crate::error::Result<()> {
tracing::trace!("Clearing all doc cache entries");
self.cache.clear().await
}
#[must_use]
pub fn stats(&self) -> &CacheStats {
&self.stats
}
#[must_use]
pub fn ttl(&self) -> &DocCacheTtl {
&self.ttl
}
}
impl Default for DocCache {
fn default() -> Self {
let cache = Arc::new(crate::cache::memory::MemoryCache::new(1000));
Self::new(cache)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::memory::MemoryCache;
#[tokio::test]
async fn test_doc_cache() {
let memory_cache = MemoryCache::new(100);
let cache = Arc::new(memory_cache);
let doc_cache = DocCache::new(cache);
doc_cache
.set_crate_docs("serde", Some("1.0"), "Test docs".to_string())
.await
.expect("set_crate_docs should succeed");
let cached = doc_cache.get_crate_docs("serde", Some("1.0")).await;
assert_eq!(
cached.as_ref().map(std::convert::AsRef::as_ref),
Some("Test docs")
);
doc_cache
.set_search_results(
"web framework",
10,
Some("relevance"),
"Search results".to_string(),
)
.await
.expect("set_search_results should succeed");
let search_cached = doc_cache
.get_search_results("web framework", 10, Some("relevance"))
.await;
assert_eq!(
search_cached.as_ref().map(std::convert::AsRef::as_ref),
Some("Search results")
);
doc_cache
.set_item_docs(
"serde",
"serde::Serialize",
Some("1.0"),
"Item docs".to_string(),
)
.await
.expect("set_item_docs should succeed");
let item_cached = doc_cache
.get_item_docs("serde", "serde::Serialize", Some("1.0"))
.await;
assert_eq!(
item_cached.as_ref().map(std::convert::AsRef::as_ref),
Some("Item docs")
);
doc_cache.clear().await.expect("clear should succeed");
let cleared = doc_cache.get_crate_docs("serde", Some("1.0")).await;
assert_eq!(cleared, None);
}
#[tokio::test]
async fn test_doc_cache_getters_preserve_shared_ownership() {
let memory_cache = Arc::new(MemoryCache::new(100));
let doc_cache = DocCache::new(memory_cache.clone());
doc_cache
.set_crate_docs("serde", Some("1.0"), "Test docs".to_string())
.await
.expect("set_crate_docs should succeed");
let key = CacheKeyGenerator::crate_cache_key("serde", Some("1.0"));
let cached_from_doc_cache = doc_cache
.get_crate_docs("serde", Some("1.0"))
.await
.expect("doc cache should return cached docs");
let cached_from_backend = memory_cache
.get(&key)
.await
.expect("backend cache should return cached docs");
assert!(Arc::ptr_eq(&cached_from_doc_cache, &cached_from_backend));
}
#[tokio::test]
async fn test_doc_cache_with_ttl() {
let memory_cache = MemoryCache::new(100);
let cache = Arc::new(memory_cache);
let mut ttl = DocCacheTtl::default();
ttl.crate_docs_secs = 7200;
ttl.search_results_secs = 600;
ttl.item_docs_secs = 3600;
ttl.set_jitter_ratio(0.0);
let doc_cache = DocCache::with_ttl(cache, ttl);
assert_eq!(doc_cache.ttl().crate_docs_secs, 7200);
assert_eq!(doc_cache.ttl().search_results_secs, 600);
assert_eq!(doc_cache.ttl().item_docs_secs, 3600);
}
#[tokio::test]
async fn test_doc_cache_stats() {
let memory_cache = MemoryCache::new(100);
let cache = Arc::new(memory_cache);
let doc_cache = DocCache::new(cache);
doc_cache
.set_crate_docs("serde", None, "docs".to_string())
.await
.ok();
doc_cache.get_crate_docs("serde", None).await;
doc_cache.get_crate_docs("nonexistent", None).await;
assert_eq!(doc_cache.stats().hits(), 1);
assert_eq!(doc_cache.stats().misses(), 1);
assert_eq!(doc_cache.stats().sets(), 1);
}
#[test]
fn test_doc_cache_default() {
let doc_cache = DocCache::default();
assert_eq!(doc_cache.ttl().crate_docs_secs, 3600);
assert_eq!(doc_cache.ttl().search_results_secs, 300);
assert_eq!(doc_cache.ttl().item_docs_secs, 1800);
}
}