Skip to main content

crates_docs/tools/docs/
cache.rs

1//! 文档缓存模块
2//!
3//! 提供文档专用的缓存服务,支持 crate 文档、搜索结果和项目文档的独立 TTL 配置。
4//!
5//! # 缓存键格式
6//!
7//! - Crate 文档: `crate:{name}` 或 `crate:{name}:{version}`
8//! - 搜索结果: `search:{query}:{limit}`
9//! - 项目文档: `item:{crate}:{path}` 或 `item:{crate}:{version}:{path}`
10//!
11//! # 示例
12//!
13//! ```rust,no_run
14//! use std::sync::Arc;
15//! use crates_docs::tools::docs::cache::{DocCache, DocCacheTtl};
16//! use crates_docs::cache::memory::MemoryCache;
17//!
18//! let cache = Arc::new(MemoryCache::new(1000));
19//! let doc_cache = DocCache::new(cache);
20//! ```
21
22use crate::cache::Cache;
23use std::sync::Arc;
24use std::time::Duration;
25
26/// 文档缓存 TTL 配置
27///
28/// 为不同类型的文档配置独立的 TTL。
29///
30/// # 字段
31///
32/// - `crate_docs_secs`: crate 文档缓存时间(秒)
33/// - `search_results_secs`: 搜索结果缓存时间(秒)
34/// - `item_docs_secs`: 项目文档缓存时间(秒)
35/// - `jitter_ratio`: TTL 抖动比例(0.0-1.0),用于防止缓存雪崩
36#[derive(Debug, Clone, Copy)]
37pub struct DocCacheTtl {
38    /// crate 文档 TTL(秒)
39    pub crate_docs_secs: u64,
40    /// 搜索结果 TTL(秒)
41    pub search_results_secs: u64,
42    /// 项目文档 TTL(秒)
43    pub item_docs_secs: u64,
44    /// TTL 抖动比例(0.0-1.0),默认 0.1(10%)
45    ///
46    /// 实际 TTL = `base_ttl * (1 + random(-jitter_ratio, jitter_ratio))`
47    /// 例如:`base_ttl=3600`, `jitter_ratio=0.1` => 实际 TTL 范围 `[3240, 3960]`
48    pub jitter_ratio: f64,
49}
50
51/// 默认 TTL 抖动比例(10%)
52const DEFAULT_JITTER_RATIO: f64 = 0.1;
53
54impl Default for DocCacheTtl {
55    fn default() -> Self {
56        Self {
57            crate_docs_secs: 3600,    // 1 小时
58            search_results_secs: 300, // 5 分钟
59            item_docs_secs: 1800,     // 30 分钟
60            jitter_ratio: DEFAULT_JITTER_RATIO,
61        }
62    }
63}
64
65impl DocCacheTtl {
66    /// 从 `CacheConfig` 创建 TTL 配置
67    ///
68    /// # 参数
69    ///
70    /// * `config` - 缓存配置
71    ///
72    /// # 返回值
73    ///
74    /// 返回根据配置创建的 TTL 配置
75    #[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    /// 计算带抖动的实际 TTL
86    ///
87    /// # 参数
88    ///
89    /// * `base_ttl` - 基础 TTL(秒)
90    ///
91    /// # 返回值
92    ///
93    /// 返回带抖动的实际 TTL(秒)
94    #[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        // 限制 jitter_ratio 在 [0.0, 1.0] 范围内
104        let ratio = self.jitter_ratio.clamp(0.0, 1.0);
105
106        // 生成 [-ratio, +ratio] 范围内的随机偏移
107        let rng = fastrand::f64();
108        let offset = (rng * 2.0 - 1.0) * ratio;
109
110        // 计算抖动后的 TTL,确保至少为 1 秒
111        (base_ttl as f64 * (1.0 + offset)).max(1.0) as u64
112    }
113}
114
115/// 文档缓存服务
116///
117/// 提供文档专用的缓存操作,支持 crate 文档、搜索结果和项目文档。
118///
119/// # 字段
120///
121/// - `cache`: 底层缓存实例
122/// - `ttl`: TTL 配置
123#[derive(Clone)]
124pub struct DocCache {
125    cache: Arc<dyn Cache>,
126    ttl: DocCacheTtl,
127}
128
129impl DocCache {
130    /// 创建新的文档缓存(使用默认 TTL)
131    ///
132    /// # 参数
133    ///
134    /// * `cache` - 缓存实例
135    ///
136    /// # 示例
137    ///
138    /// ```rust,no_run
139    /// use std::sync::Arc;
140    /// use crates_docs::tools::docs::cache::DocCache;
141    /// use crates_docs::cache::memory::MemoryCache;
142    ///
143    /// let cache = Arc::new(MemoryCache::new(1000));
144    /// let doc_cache = DocCache::new(cache);
145    /// ```
146    pub fn new(cache: Arc<dyn Cache>) -> Self {
147        Self {
148            cache,
149            ttl: DocCacheTtl::default(),
150        }
151    }
152
153    /// 创建新的文档缓存(使用自定义 TTL)
154    ///
155    /// # 参数
156    ///
157    /// * `cache` - 缓存实例
158    /// * `ttl` - TTL 配置
159    ///
160    /// # 示例
161    ///
162    /// ```rust,no_run
163    /// use std::sync::Arc;
164    /// use crates_docs::tools::docs::cache::{DocCache, DocCacheTtl};
165    /// use crates_docs::cache::memory::MemoryCache;
166    ///
167    /// let cache = Arc::new(MemoryCache::new(1000));
168    /// let ttl = DocCacheTtl {
169    ///     crate_docs_secs: 7200,
170    ///     search_results_secs: 600,
171    ///     item_docs_secs: 3600,
172    ///     jitter_ratio: 0.1,
173    /// };
174    /// let doc_cache = DocCache::with_ttl(cache, ttl);
175    /// ```
176    #[must_use]
177    pub fn with_ttl(cache: Arc<dyn Cache>, ttl: DocCacheTtl) -> Self {
178        Self { cache, ttl }
179    }
180
181    /// 获取缓存的 crate 文档
182    ///
183    /// # 参数
184    ///
185    /// * `crate_name` - crate 名称
186    /// * `version` - 可选的版本号
187    ///
188    /// # 返回值
189    ///
190    /// 如果缓存命中,返回文档内容;否则返回 `None`
191    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    /// 设置 crate 文档缓存
197    ///
198    /// # 参数
199    ///
200    /// * `crate_name` - crate 名称
201    /// * `version` - 可选的版本号
202    /// * `content` - 文档内容
203    ///
204    /// # 错误
205    ///
206    /// 如果缓存操作失败,返回错误
207    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    /// 获取缓存的搜索结果
219    ///
220    /// # 参数
221    ///
222    /// * `query` - 搜索查询
223    /// * `limit` - 结果数量限制
224    ///
225    /// # 返回值
226    ///
227    /// 如果缓存命中,返回搜索结果;否则返回 `None`
228    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    /// 设置搜索结果缓存
234    ///
235    /// # 参数
236    ///
237    /// * `query` - 搜索查询
238    /// * `limit` - 结果数量限制
239    /// * `content` - 搜索结果内容
240    ///
241    /// # 错误
242    ///
243    /// 如果缓存操作失败,返回错误
244    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    /// 获取缓存的项目文档
256    ///
257    /// # 参数
258    ///
259    /// * `crate_name` - crate 名称
260    /// * `item_path` - 项目路径
261    /// * `version` - 可选的版本号
262    ///
263    /// # 返回值
264    ///
265    /// 如果缓存命中,返回项目文档;否则返回 `None`
266    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    /// 设置项目文档缓存
277    ///
278    /// # 参数
279    ///
280    /// * `crate_name` - crate 名称
281    /// * `item_path` - 项目路径
282    /// * `version` - 可选的版本号
283    /// * `content` - 文档内容
284    ///
285    /// # 错误
286    ///
287    /// 如果缓存操作失败,返回错误
288    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    /// 清除缓存
301    ///
302    /// # 错误
303    ///
304    /// 如果缓存操作失败,返回错误
305    pub async fn clear(&self) -> crate::error::Result<()> {
306        self.cache.clear().await
307    }
308
309    /// 构建 crate 缓存键
310    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    /// 构建搜索缓存键
319    fn search_cache_key(query: &str, limit: u32) -> String {
320        format!("search:{query}:{limit}")
321    }
322
323    /// 构建项目缓存键
324    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        // 测试 crate 文档缓存
352        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        // 测试搜索结果缓存
360        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        // 测试项目文档缓存
368        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        // 测试清理
383        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}