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