crates_docs/cache/mod.rs
1//! 缓存模块
2//!
3//! 提供内存缓存和 Redis 缓存支持。
4//!
5//! # 特性
6//!
7//! - **内存缓存**: 基于 `moka` 的高性能内存缓存,支持 `TinyLFU` 淘汰策略
8//! - **Redis 缓存**: 支持分布式部署(需要启用 `cache-redis` feature)
9//!
10//! # 示例
11//!
12//! ```rust,no_run
13//! use crates_docs::cache::{Cache, CacheConfig, create_cache};
14//!
15//! let config = CacheConfig::default();
16//! let cache = create_cache(&config).expect("Failed to create cache");
17//! ```
18
19#[cfg(feature = "cache-memory")]
20pub mod memory;
21
22#[cfg(feature = "cache-redis")]
23pub mod redis;
24
25use std::time::Duration;
26
27/// 缓存 trait
28///
29/// 定义缓存操作的基本接口,支持异步读写、TTL 过期和批量清理。
30///
31/// # 实现
32///
33/// - `memory::MemoryCache`: 内存缓存实现
34/// - `redis::RedisCache`: Redis 缓存实现(需要 `cache-redis` feature)
35#[async_trait::async_trait]
36pub trait Cache: Send + Sync {
37 /// 获取缓存值
38 ///
39 /// # 参数
40 ///
41 /// * `key` - 缓存键
42 ///
43 /// # 返回值
44 ///
45 /// 如果键存在且未过期,返回缓存值;否则返回 `None`
46 async fn get(&self, key: &str) -> Option<String>;
47
48 /// 设置缓存值
49 ///
50 /// # 参数
51 ///
52 /// * `key` - 缓存键
53 /// * `value` - 缓存值
54 /// * `ttl` - 可选的过期时间
55 ///
56 /// # 错误
57 ///
58 /// 如果缓存操作失败,返回错误
59 async fn set(
60 &self,
61 key: String,
62 value: String,
63 ttl: Option<Duration>,
64 ) -> crate::error::Result<()>;
65
66 /// 删除缓存值
67 ///
68 /// # 参数
69 ///
70 /// * `key` - 缓存键
71 ///
72 /// # 错误
73 ///
74 /// 如果缓存操作失败,返回错误
75 async fn delete(&self, key: &str) -> crate::error::Result<()>;
76
77 /// 清除所有缓存条目
78 ///
79 /// 仅清除配置了前缀的缓存条目。
80 ///
81 /// # 错误
82 ///
83 /// 如果缓存操作失败,返回错误
84 async fn clear(&self) -> crate::error::Result<()>;
85
86 /// 检查键是否存在
87 ///
88 /// # 参数
89 ///
90 /// * `key` - 缓存键
91 ///
92 /// # 返回值
93 ///
94 /// 如果键存在返回 `true`,否则返回 `false`
95 async fn exists(&self, key: &str) -> bool;
96}
97
98/// 缓存配置
99///
100/// 配置缓存类型、大小、TTL 等参数。
101///
102/// # 字段
103///
104/// - `cache_type`: 缓存类型,`"memory"` 或 `"redis"`
105/// - `memory_size`: 内存缓存大小(条目数)
106/// - `redis_url`: Redis 连接 URL
107/// - `key_prefix`: 键前缀(用于隔离不同服务的缓存)
108/// - `default_ttl`: 默认 TTL(秒)
109/// - `crate_docs_ttl_secs`: crate 文档缓存 TTL(秒)
110/// - `item_docs_ttl_secs`: 项目文档缓存 TTL(秒)
111/// - `search_results_ttl_secs`: 搜索结果缓存 TTL(秒)
112///
113/// # 热重载支持
114///
115/// ## 支持热重载的字段 ✅
116///
117/// TTL 相关字段可以在运行时动态更新(影响新写入的缓存条目):
118/// - `default_ttl`: 默认 TTL(秒)
119/// - `crate_docs_ttl_secs`: crate 文档缓存 TTL(秒)
120/// - `item_docs_ttl_secs`: 项目文档缓存 TTL(秒)
121/// - `search_results_ttl_secs`: 搜索结果缓存 TTL(秒)
122///
123/// ## 不支持热重载的字段 ❌
124///
125/// 以下字段需要重启服务器才能生效:
126/// - `cache_type`: 缓存类型(涉及缓存实例的创建)
127/// - `memory_size`: 内存缓存大小(初始化参数)
128/// - `redis_url`: Redis 连接 URL(连接池初始化)
129/// - `key_prefix`: 缓存键前缀(初始化参数)
130///
131/// 原因:这些配置涉及缓存后端(内存/R)的初始化和连接池创建。
132#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
133pub struct CacheConfig {
134 /// 缓存类型:`memory` 或 `redis`
135 pub cache_type: String,
136
137 /// 内存缓存大小(条目数)
138 pub memory_size: Option<usize>,
139
140 /// Redis 连接 URL
141 pub redis_url: Option<String>,
142
143 /// Redis 缓存键前缀(用于隔离不同服务的缓存条目)
144 #[serde(default = "default_key_prefix")]
145 pub key_prefix: String,
146
147 /// 默认 TTL(秒)
148 pub default_ttl: Option<u64>,
149
150 /// crate 文档缓存 TTL(秒)
151 #[serde(default = "default_crate_docs_ttl")]
152 pub crate_docs_ttl_secs: Option<u64>,
153
154 /// 项目文档缓存 TTL(秒)
155 #[serde(default = "default_item_docs_ttl")]
156 pub item_docs_ttl_secs: Option<u64>,
157
158 /// 搜索结果缓存 TTL(秒)
159 #[serde(default = "default_search_results_ttl")]
160 pub search_results_ttl_secs: Option<u64>,
161}
162
163/// 默认 crate 文档 TTL(1 小时)
164#[must_use]
165pub fn default_crate_docs_ttl() -> Option<u64> {
166 Some(3600)
167}
168
169/// 默认项目文档 TTL(30 分钟)
170#[must_use]
171pub fn default_item_docs_ttl() -> Option<u64> {
172 Some(1800)
173}
174
175/// 默认搜索结果 TTL(5 分钟)
176#[must_use]
177pub fn default_search_results_ttl() -> Option<u64> {
178 Some(300)
179}
180
181/// 默认键前缀
182#[must_use]
183pub fn default_key_prefix() -> String {
184 String::new()
185}
186
187impl Default for CacheConfig {
188 fn default() -> Self {
189 Self {
190 cache_type: "memory".to_string(),
191 memory_size: Some(1000),
192 redis_url: None,
193 key_prefix: String::new(),
194 default_ttl: Some(3600), // 1 hour
195 crate_docs_ttl_secs: default_crate_docs_ttl(),
196 item_docs_ttl_secs: default_item_docs_ttl(),
197 search_results_ttl_secs: default_search_results_ttl(),
198 }
199 }
200}
201
202/// 创建缓存实例
203///
204/// # 参数
205///
206/// * `config` - 缓存配置
207///
208/// # 错误
209///
210/// 如果缓存类型不支持或配置无效,返回错误
211///
212/// # 示例
213///
214/// ```rust,no_run
215/// use crates_docs::cache::{CacheConfig, create_cache};
216///
217/// let config = CacheConfig::default();
218/// let cache = create_cache(&config).expect("Failed to create cache");
219/// ```
220pub fn create_cache(config: &CacheConfig) -> Result<Box<dyn Cache>, crate::error::Error> {
221 match config.cache_type.as_str() {
222 "memory" => {
223 #[cfg(feature = "cache-memory")]
224 {
225 let size = config.memory_size.unwrap_or(1000);
226 Ok(Box::new(memory::MemoryCache::new(size)))
227 }
228 #[cfg(not(feature = "cache-memory"))]
229 {
230 Err(crate::error::Error::config(
231 "cache_type",
232 "memory cache feature is not enabled",
233 ))
234 }
235 }
236 "redis" => {
237 #[cfg(feature = "cache-redis")]
238 {
239 // Note: Redis cache requires async initialization, this returns a placeholder
240 // In practice, use the create_cache_async function
241 Err(crate::error::Error::config(
242 "cache_type",
243 "Redis cache requires async initialization. Use create_cache_async instead.",
244 ))
245 }
246 #[cfg(not(feature = "cache-redis"))]
247 {
248 Err(crate::error::Error::config(
249 "cache_type",
250 "redis cache feature is not enabled",
251 ))
252 }
253 }
254 _ => Err(crate::error::Error::config(
255 "cache_type",
256 format!("unsupported cache type: {}", config.cache_type),
257 )),
258 }
259}
260
261/// 异步创建缓存实例
262///
263/// 支持 Redis 缓存的异步初始化。
264///
265/// # 参数
266///
267/// * `config` - 缓存配置
268///
269/// # 错误
270///
271/// 如果缓存类型不支持或配置无效,返回错误
272///
273/// # 示例
274///
275/// ```rust,no_run
276/// use crates_docs::cache::{CacheConfig, create_cache_async};
277///
278/// #[tokio::main]
279/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
280/// let config = CacheConfig::default();
281/// let cache = create_cache_async(&config).await?;
282/// Ok(())
283/// }
284/// ```
285#[cfg(feature = "cache-redis")]
286pub async fn create_cache_async(
287 config: &CacheConfig,
288) -> Result<Box<dyn Cache>, crate::error::Error> {
289 match config.cache_type.as_str() {
290 "memory" => {
291 let size = config.memory_size.unwrap_or(1000);
292 Ok(Box::new(memory::MemoryCache::new(size)))
293 }
294 "redis" => {
295 let url = config
296 .redis_url
297 .as_ref()
298 .ok_or_else(|| crate::error::Error::config("redis_url", "redis_url is required"))?;
299 Ok(Box::new(
300 redis::RedisCache::new(url, config.key_prefix.clone()).await?,
301 ))
302 }
303 _ => Err(crate::error::Error::config(
304 "cache_type",
305 format!("unsupported cache type: {}", config.cache_type),
306 )),
307 }
308}