Skip to main content

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    /// Convert to Any for downcasting (used in tests)
147    ///
148    /// This method allows downcasting the cache to its concrete type
149    /// for accessing test-only methods like `run_pending_tasks`.
150    fn as_any(&self) -> &dyn std::any::Any;
151}
152
153/// Cache configuration
154///
155/// Configure cache type, size, TTL, and other parameters.
156///
157/// # Fields
158///
159/// - `cache_type`: Cache type, `"memory"` or `"redis"`
160/// - `memory_size`: Memory cache size(number of entries)
161/// - `redis_url`: Redis connection URL
162/// - `key_prefix`: Key prefix (used to isolate caches of different services)
163/// - `default_ttl`: Default TTL (seconds)
164/// - `crate_docs_ttl_secs`: Crate document cache TTL (seconds)
165/// - `item_docs_ttl_secs`: Item document cache TTL (seconds)
166/// - `search_results_ttl_secs`: Search result cache TTL (seconds)
167///
168/// # Hot reload support
169///
170/// ## Hot reload supported fields ✅
171///
172/// TTL-related fields can be dynamically updated at runtime (affecting newly written cache entries):
173/// - `default_ttl`: Default TTL (seconds)
174/// - `crate_docs_ttl_secs`: Crate document cache TTL (seconds)
175/// - `item_docs_ttl_secs`: Item document cache TTL (seconds)
176/// - `search_results_ttl_secs`: Search result cache TTL (seconds)
177///
178/// ## Hot reload NOT supported fields ❌
179///
180/// The following fields require server restart to take effect:
181/// - `cache_type`: Cache type (involves cache instance creation)
182/// - `memory_size`: Memory cache size(initialization parameter)
183/// - `redis_url`: Redis connection URL(connection pool initialization)
184/// - `key_prefix`: Cache key prefix(initialization parameter)
185///
186/// Reason: These configurations involve initialization of cache backend (memory/Redis) and connection pool creation.
187#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
188pub struct CacheConfig {
189    /// Cache type: `memory` or `redis`
190    #[serde(default = "default_cache_cache_type")]
191    pub cache_type: String,
192
193    /// Memory cache size(number of entries)
194    #[serde(default)]
195    pub memory_size: Option<usize>,
196
197    /// Redis connection URL
198    #[serde(default)]
199    pub redis_url: Option<String>,
200
201    /// Redis cache key prefix (used to isolate caches of different services)
202    #[serde(default = "default_key_prefix")]
203    pub key_prefix: String,
204
205    /// Default TTL (seconds)
206    #[serde(default)]
207    pub default_ttl: Option<u64>,
208
209    /// Crate document cache TTL (seconds)
210    #[serde(default = "default_crate_docs_ttl")]
211    pub crate_docs_ttl_secs: Option<u64>,
212
213    /// Item document cache TTL (seconds)
214    #[serde(default = "default_item_docs_ttl")]
215    pub item_docs_ttl_secs: Option<u64>,
216
217    /// Search result cache TTL (seconds)
218    #[serde(default = "default_search_results_ttl")]
219    pub search_results_ttl_secs: Option<u64>,
220}
221
222/// Default crate document TTL (1 hour)
223#[must_use]
224pub fn default_crate_docs_ttl() -> Option<u64> {
225    Some(DEFAULT_CRATE_DOCS_TTL_SECS)
226}
227
228/// Default item document TTL (30 minutes)
229#[must_use]
230pub fn default_item_docs_ttl() -> Option<u64> {
231    Some(DEFAULT_ITEM_DOCS_TTL_SECS)
232}
233
234/// Default search result TTL (5 minutes)
235#[must_use]
236pub fn default_search_results_ttl() -> Option<u64> {
237    Some(DEFAULT_SEARCH_RESULTS_TTL_SECS)
238}
239
240/// Default key prefix
241#[must_use]
242pub fn default_key_prefix() -> String {
243    String::new()
244}
245
246impl Default for CacheConfig {
247    fn default() -> Self {
248        Self {
249            cache_type: "memory".to_string(),
250            memory_size: Some(DEFAULT_MEMORY_CACHE_SIZE),
251            redis_url: None,
252            key_prefix: String::new(),
253            default_ttl: Some(DEFAULT_CRATE_DOCS_TTL_SECS),
254            crate_docs_ttl_secs: default_crate_docs_ttl(),
255            item_docs_ttl_secs: default_item_docs_ttl(),
256            search_results_ttl_secs: default_search_results_ttl(),
257        }
258    }
259}
260
261fn default_cache_cache_type() -> String {
262    CacheConfig::default().cache_type
263}
264
265/// Create cache instance
266///
267/// # Arguments
268///
269/// * `config` - Cache configuration
270///
271/// # Errors
272///
273/// Returns error if cache type is not supported or configuration is invalid
274///
275/// # Examples
276///
277/// ```rust,no_run
278/// use crates_docs::cache::{CacheConfig, create_cache};
279///
280/// let config = CacheConfig::default();
281/// let cache = create_cache(&config).expect("Failed to create cache");
282/// ```
283pub fn create_cache(config: &CacheConfig) -> Result<Box<dyn Cache>, crate::error::Error> {
284    match config.cache_type.as_str() {
285        "memory" => {
286            #[cfg(feature = "cache-memory")]
287            {
288                let size = config.memory_size.unwrap_or(DEFAULT_MEMORY_CACHE_SIZE);
289                Ok(Box::new(memory::MemoryCache::new(size)))
290            }
291            #[cfg(not(feature = "cache-memory"))]
292            {
293                Err(crate::error::Error::config(
294                    "cache_type",
295                    "memory cache feature is not enabled",
296                ))
297            }
298        }
299        "redis" => {
300            #[cfg(feature = "cache-redis")]
301            {
302                // Note: Redis cache requires async initialization, this returns a placeholder
303                // In practice, use the create_cache_async function
304                Err(crate::error::Error::config(
305                    "cache_type",
306                    "Redis cache requires async initialization. Use create_cache_async instead.",
307                ))
308            }
309            #[cfg(not(feature = "cache-redis"))]
310            {
311                Err(crate::error::Error::config(
312                    "cache_type",
313                    "redis cache feature is not enabled",
314                ))
315            }
316        }
317        _ => Err(crate::error::Error::config(
318            "cache_type",
319            format!("unsupported cache type: {}", config.cache_type),
320        )),
321    }
322}
323
324/// Async create cache instance
325///
326/// Supports async initialization for Redis cache.
327///
328/// # Arguments
329///
330/// * `config` - Cache configuration
331///
332/// # Errors
333///
334/// Returns error if cache type is not supported or configuration is invalid
335///
336/// # Examples
337///
338/// ```rust,no_run
339/// use crates_docs::cache::{CacheConfig, create_cache_async};
340///
341/// #[tokio::main]
342/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
343///     let config = CacheConfig::default();
344///     let cache = create_cache_async(&config).await?;
345///     Ok(())
346/// }
347/// ```
348#[cfg(feature = "cache-redis")]
349pub async fn create_cache_async(
350    config: &CacheConfig,
351) -> Result<Box<dyn Cache>, crate::error::Error> {
352    match config.cache_type.as_str() {
353        "memory" => {
354            let size = config.memory_size.unwrap_or(DEFAULT_MEMORY_CACHE_SIZE);
355            Ok(Box::new(memory::MemoryCache::new(size)))
356        }
357        "redis" => {
358            let url = config
359                .redis_url
360                .as_ref()
361                .ok_or_else(|| crate::error::Error::config("redis_url", "redis_url is required"))?;
362            Ok(Box::new(
363                redis::RedisCache::new(url, config.key_prefix.clone()).await?,
364            ))
365        }
366        _ => Err(crate::error::Error::config(
367            "cache_type",
368            format!("unsupported cache type: {}", config.cache_type),
369        )),
370    }
371}