crates_docs/cache/mod.rs
1//! Cache module
2//!
3//! Provides memory cache and Redis cache support.
4//!
5//! # Features
6//!
7//! - **Memory cache**: High-performance memory cache based on `moka`, supporting `TinyLFU` eviction strategy
8//! - **Redis cache**: Supports distributed deployment (requires `cache-redis` feature)
9//!
10//! # Examples
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::sync::Arc;
26use std::time::Duration;
27
28/// Default memory cache capacity
29///
30/// # Value
31///
32/// 1000 entries
33///
34/// # Rationale
35///
36/// Provides good balance between memory usage and cache hit rate for typical workloads.
37/// Configurable via `CacheConfig::memory_size`.
38const DEFAULT_MEMORY_CACHE_SIZE: usize = 1000;
39
40/// Default crate documentation TTL in seconds
41///
42/// # Value
43///
44/// 3600 seconds (1 hour)
45///
46/// # Rationale
47///
48/// Reused from ttl.rs for consistency. Crate documentation changes infrequently.
49/// Configurable via `CacheConfig::crate_docs_ttl_secs`.
50const DEFAULT_CRATE_DOCS_TTL_SECS: u64 = 3600;
51
52/// Default item documentation TTL in seconds
53///
54/// # Value
55///
56/// 1800 seconds (30 minutes)
57///
58/// # Rationale
59///
60/// Reused from ttl.rs for consistency. Item documentation changes moderately often.
61/// Configurable via `CacheConfig::item_docs_ttl_secs`.
62const DEFAULT_ITEM_DOCS_TTL_SECS: u64 = 1800;
63
64/// Default search results TTL in seconds
65///
66/// # Value
67///
68/// 300 seconds (5 minutes)
69///
70/// # Rationale
71///
72/// Reused from ttl.rs for consistency. Search results change frequently.
73/// Configurable via `CacheConfig::search_results_ttl_secs`.
74const DEFAULT_SEARCH_RESULTS_TTL_SECS: u64 = 300;
75
76/// Cache trait
77///
78/// Defines basic cache operation interface, supporting async read/write, TTL expiration, and bulk cleanup.
79///
80/// # Implementations
81///
82/// - `memory::MemoryCache`: Memory cache implementation
83/// - `redis::RedisCache`: Redis cache implementation (requires `cache-redis` feature)
84#[async_trait::async_trait]
85pub trait Cache: Send + Sync {
86 /// Get cache value
87 ///
88 /// # Arguments
89 ///
90 /// * `key` - Cache key
91 ///
92 /// # Returns
93 ///
94 /// If key exists and not expired, returns `Arc<str>` to avoid cloning; otherwise returns `None`
95 async fn get(&self, key: &str) -> Option<Arc<str>>;
96
97 /// Set cache value
98 ///
99 /// # Arguments
100 ///
101 /// * `key` - Cache key
102 /// * `value` - Cache value
103 /// * `ttl` - Optional expiration time
104 ///
105 /// # Errors
106 ///
107 /// Returns error if cache operation fails
108 async fn set(
109 &self,
110 key: String,
111 value: String,
112 ttl: Option<Duration>,
113 ) -> crate::error::Result<()>;
114
115 /// Delete cache value
116 ///
117 /// # Arguments
118 ///
119 /// * `key` - Cache key
120 ///
121 /// # Errors
122 ///
123 /// Returns error if cache operation fails
124 async fn delete(&self, key: &str) -> crate::error::Result<()>;
125
126 /// Clear all cache entries
127 ///
128 /// Clears only cache entries with configured prefix.
129 ///
130 /// # Errors
131 ///
132 /// Returns error if cache operation fails
133 async fn clear(&self) -> crate::error::Result<()>;
134
135 /// Check if key exists
136 ///
137 /// # Arguments
138 ///
139 /// * `key` - Cache key
140 ///
141 /// # Returns
142 ///
143 /// Returns `true` if key exists, otherwise `false`
144 async fn exists(&self, key: &str) -> bool;
145}
146
147/// Cache configuration
148///
149/// Configure cache type, size, TTL, and other parameters.
150///
151/// # Fields
152///
153/// - `cache_type`: Cache type, `"memory"` or `"redis"`
154/// - `memory_size`: Memory cache size(number of entries)
155/// - `redis_url`: Redis connection URL
156/// - `key_prefix`: Key prefix (used to isolate caches of different services)
157/// - `default_ttl`: Default TTL (seconds)
158/// - `crate_docs_ttl_secs`: Crate document cache TTL (seconds)
159/// - `item_docs_ttl_secs`: Item document cache TTL (seconds)
160/// - `search_results_ttl_secs`: Search result cache TTL (seconds)
161///
162/// # Hot reload support
163///
164/// ## Hot reload supported fields ✅
165///
166/// TTL-related fields can be dynamically updated at runtime (affecting newly written cache entries):
167/// - `default_ttl`: Default TTL (seconds)
168/// - `crate_docs_ttl_secs`: Crate document cache TTL (seconds)
169/// - `item_docs_ttl_secs`: Item document cache TTL (seconds)
170/// - `search_results_ttl_secs`: Search result cache TTL (seconds)
171///
172/// ## Hot reload NOT supported fields ❌
173///
174/// The following fields require server restart to take effect:
175/// - `cache_type`: Cache type (involves cache instance creation)
176/// - `memory_size`: Memory cache size(initialization parameter)
177/// - `redis_url`: Redis connection URL(connection pool initialization)
178/// - `key_prefix`: Cache key prefix(initialization parameter)
179///
180/// Reason: These configurations involve initialization of cache backend (memory/Redis) and connection pool creation.
181#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
182pub struct CacheConfig {
183 /// Cache type: `memory` or `redis`
184 pub cache_type: String,
185
186 /// Memory cache size(number of entries)
187 pub memory_size: Option<usize>,
188
189 /// Redis connection URL
190 pub redis_url: Option<String>,
191
192 /// Redis cache key prefix (used to isolate caches of different services)
193 #[serde(default = "default_key_prefix")]
194 pub key_prefix: String,
195
196 /// Default TTL (seconds)
197 pub default_ttl: Option<u64>,
198
199 /// Crate document cache TTL (seconds)
200 #[serde(default = "default_crate_docs_ttl")]
201 pub crate_docs_ttl_secs: Option<u64>,
202
203 /// Item document cache TTL (seconds)
204 #[serde(default = "default_item_docs_ttl")]
205 pub item_docs_ttl_secs: Option<u64>,
206
207 /// Search result cache TTL (seconds)
208 #[serde(default = "default_search_results_ttl")]
209 pub search_results_ttl_secs: Option<u64>,
210}
211
212/// Default crate document TTL (1 hour)
213#[must_use]
214pub fn default_crate_docs_ttl() -> Option<u64> {
215 Some(DEFAULT_CRATE_DOCS_TTL_SECS)
216}
217
218/// Default item document TTL (30 minutes)
219#[must_use]
220pub fn default_item_docs_ttl() -> Option<u64> {
221 Some(DEFAULT_ITEM_DOCS_TTL_SECS)
222}
223
224/// Default search result TTL (5 minutes)
225#[must_use]
226pub fn default_search_results_ttl() -> Option<u64> {
227 Some(DEFAULT_SEARCH_RESULTS_TTL_SECS)
228}
229
230/// Default key prefix
231#[must_use]
232pub fn default_key_prefix() -> String {
233 String::new()
234}
235
236impl Default for CacheConfig {
237 fn default() -> Self {
238 Self {
239 cache_type: "memory".to_string(),
240 memory_size: Some(DEFAULT_MEMORY_CACHE_SIZE),
241 redis_url: None,
242 key_prefix: String::new(),
243 default_ttl: Some(DEFAULT_CRATE_DOCS_TTL_SECS),
244 crate_docs_ttl_secs: default_crate_docs_ttl(),
245 item_docs_ttl_secs: default_item_docs_ttl(),
246 search_results_ttl_secs: default_search_results_ttl(),
247 }
248 }
249}
250
251/// Create cache instance
252///
253/// # Arguments
254///
255/// * `config` - Cache configuration
256///
257/// # Errors
258///
259/// Returns error if cache type is not supported or configuration is invalid
260///
261/// # Examples
262///
263/// ```rust,no_run
264/// use crates_docs::cache::{CacheConfig, create_cache};
265///
266/// let config = CacheConfig::default();
267/// let cache = create_cache(&config).expect("Failed to create cache");
268/// ```
269pub fn create_cache(config: &CacheConfig) -> Result<Box<dyn Cache>, crate::error::Error> {
270 match config.cache_type.as_str() {
271 "memory" => {
272 #[cfg(feature = "cache-memory")]
273 {
274 let size = config.memory_size.unwrap_or(DEFAULT_MEMORY_CACHE_SIZE);
275 Ok(Box::new(memory::MemoryCache::new(size)))
276 }
277 #[cfg(not(feature = "cache-memory"))]
278 {
279 Err(crate::error::Error::config(
280 "cache_type",
281 "memory cache feature is not enabled",
282 ))
283 }
284 }
285 "redis" => {
286 #[cfg(feature = "cache-redis")]
287 {
288 // Note: Redis cache requires async initialization, this returns a placeholder
289 // In practice, use the create_cache_async function
290 Err(crate::error::Error::config(
291 "cache_type",
292 "Redis cache requires async initialization. Use create_cache_async instead.",
293 ))
294 }
295 #[cfg(not(feature = "cache-redis"))]
296 {
297 Err(crate::error::Error::config(
298 "cache_type",
299 "redis cache feature is not enabled",
300 ))
301 }
302 }
303 _ => Err(crate::error::Error::config(
304 "cache_type",
305 format!("unsupported cache type: {}", config.cache_type),
306 )),
307 }
308}
309
310/// Async create cache instance
311///
312/// Supports async initialization for Redis cache.
313///
314/// # Arguments
315///
316/// * `config` - Cache configuration
317///
318/// # Errors
319///
320/// Returns error if cache type is not supported or configuration is invalid
321///
322/// # Examples
323///
324/// ```rust,no_run
325/// use crates_docs::cache::{CacheConfig, create_cache_async};
326///
327/// #[tokio::main]
328/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
329/// let config = CacheConfig::default();
330/// let cache = create_cache_async(&config).await?;
331/// Ok(())
332/// }
333/// ```
334#[cfg(feature = "cache-redis")]
335pub async fn create_cache_async(
336 config: &CacheConfig,
337) -> Result<Box<dyn Cache>, crate::error::Error> {
338 match config.cache_type.as_str() {
339 "memory" => {
340 let size = config.memory_size.unwrap_or(DEFAULT_MEMORY_CACHE_SIZE);
341 Ok(Box::new(memory::MemoryCache::new(size)))
342 }
343 "redis" => {
344 let url = config
345 .redis_url
346 .as_ref()
347 .ok_or_else(|| crate::error::Error::config("redis_url", "redis_url is required"))?;
348 Ok(Box::new(
349 redis::RedisCache::new(url, config.key_prefix.clone()).await?,
350 ))
351 }
352 _ => Err(crate::error::Error::config(
353 "cache_type",
354 format!("unsupported cache type: {}", config.cache_type),
355 )),
356 }
357}