Skip to main content

fraiseql_core/cache/
config.rs

1//! Cache configuration.
2//!
3//! Defines configuration options for the query result cache.
4//!
5//! # Important: Caching is Disabled by Default (v2.0.0-rc.12+)
6//!
7//! **FraiseQL v2.0.0-rc.12 changed the default: caching is now DISABLED.**
8//!
9//! FraiseQL uses precomputed views (tv_* tables) and optimized PostgreSQL queries
10//! that are typically faster than cache overhead for most use cases. Caching only
11//! provides benefit for specific scenarios (see below).
12//!
13//! # When to Enable Caching
14//!
15//! Enable caching (`enabled = true`) **only** if you have:
16//!
17//! 1. **Federation with slow external services** (>100ms response times)
18//! 2. **Expensive computations** not covered by precomputed views
19//! 3. **Very high-frequency repeated queries** (>1000 QPS with identical parameters)
20//!
21//! # When NOT to Enable Caching
22//!
23//! Don't enable caching for:
24//! - Simple lookups (faster to query PostgreSQL directly)
25//! - Standard CRUD operations on precomputed views
26//! - Low-traffic applications (<100 QPS)
27//! - Any workload where Issue #40 analysis applies
28//!
29//! # Configuration Examples
30//!
31//! **Default (recommended for most deployments):**
32//! ```rust
33//! use fraiseql_core::cache::CacheConfig;
34//!
35//! let config = CacheConfig::default(); // enabled = false
36//! ```
37//!
38//! **Federation with external services:**
39//! ```rust
40//! use fraiseql_core::cache::CacheConfig;
41//!
42//! let config = CacheConfig::enabled();
43//! ```
44//!
45//! **Custom cache size (if enabled):**
46//! ```rust
47//! use fraiseql_core::cache::CacheConfig;
48//!
49//! let config = CacheConfig {
50//!     enabled: true,
51//!     max_entries: 5_000,
52//!     ttl_seconds: 3_600, // 1 hour
53//!     cache_list_queries: true,
54//! };
55//! ```
56//!
57//! # Memory Estimates (if enabled)
58//!
59//! - **1,000 entries**: ~10 MB
60//! - **10,000 entries**: ~100 MB
61//! - **50,000 entries**: ~500 MB
62//!
63//! Actual memory usage depends on query result sizes.
64
65use serde::{Deserialize, Serialize};
66
67/// Cache configuration - **disabled by default** as of v2.0.0-rc.12.
68///
69/// FraiseQL's architecture (precomputed views + optimized PostgreSQL) makes
70/// caching unnecessary for most use cases. Enable only for federation or
71/// expensive computations.
72///
73/// # Key Changes in rc.12
74///
75/// - `enabled` now defaults to `false` (was `true`)
76/// - `with_max_entries()` and `with_ttl()` also set `enabled: false`
77/// - New `enabled()` constructor for explicit opt-in
78///
79/// See module documentation for detailed guidance.
80#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
81pub struct CacheConfig {
82    /// Enable response caching.
83    ///
84    /// **Default: `false`** (changed from `true` in v2.0.0-rc.12)
85    ///
86    /// Enable only for:
87    /// - Federation with slow external services
88    /// - Expensive computations not covered by precomputed views
89    /// - High-frequency repeated queries with identical parameters
90    ///
91    /// See Issue #40 for performance analysis.
92    pub enabled: bool,
93
94    /// Maximum number of cached entries.
95    ///
96    /// When this limit is reached, the least-recently-used (LRU) entry is evicted
97    /// to make room for new entries. This hard limit prevents unbounded memory growth.
98    ///
99    /// Recommended values:
100    /// - Development: 1,000
101    /// - Production (small): 10,000
102    /// - Production (large): 50,000
103    ///
104    /// Default: 10,000 entries (~100 MB estimated memory)
105    pub max_entries: usize,
106
107    /// Time-to-live (TTL) in seconds for cached entries.
108    ///
109    /// Entries older than this are considered expired and will be removed on next access.
110    /// This acts as a safety net for cases where invalidation might be missed (e.g.,
111    /// database changes outside of mutations).
112    ///
113    /// Recommended values:
114    /// - Development: 3,600 (1 hour)
115    /// - Production: 86,400 (24 hours)
116    /// - Long-lived data: 604,800 (7 days)
117    ///
118    /// Default: 86,400 seconds (24 hours)
119    pub ttl_seconds: u64,
120
121    /// Whether to cache list queries.
122    ///
123    /// List queries (e.g., `users(limit: 100)`) can have large result sets that
124    /// consume significant memory. Set to `false` to only cache single-object queries.
125    ///
126    /// **Note**: Currently not implemented (all queries are cached).
127    /// This field is reserved for future use.
128    ///
129    /// Default: `true`
130    pub cache_list_queries: bool,
131}
132
133impl Default for CacheConfig {
134    /// Default cache configuration - **DISABLED by default** as of v2.0.0-rc.12.
135    ///
136    /// FraiseQL uses precomputed views (tv_* tables) and optimized PostgreSQL queries
137    /// that are typically faster than cache overhead for most use cases.
138    ///
139    /// Enable caching ONLY if you have:
140    /// - Federation with slow external services
141    /// - Expensive computations not covered by precomputed views
142    /// - High-frequency repeated queries (>1000 QPS with same params)
143    ///
144    /// See Issue #40 for performance analysis.
145    ///
146    /// # Current Default
147    /// - **Caching: DISABLED**
148    /// - 10,000 max entries (~100 MB if enabled)
149    /// - 24 hour TTL
150    /// - List queries cached (when enabled)
151    fn default() -> Self {
152        Self {
153            enabled:            false, // CHANGED in rc.12: Disabled by default
154            max_entries:        10_000,
155            ttl_seconds:        86_400, // 24 hours
156            cache_list_queries: true,
157        }
158    }
159}
160
161impl CacheConfig {
162    /// Create cache configuration with custom max entries.
163    ///
164    /// Uses default values for other fields (**enabled=false**, 24h TTL).
165    ///
166    /// # Arguments
167    ///
168    /// * `max_entries` - Maximum number of entries in cache
169    ///
170    /// # Example
171    ///
172    /// ```rust
173    /// use fraiseql_core::cache::CacheConfig;
174    ///
175    /// let config = CacheConfig::with_max_entries(50_000);
176    /// assert_eq!(config.max_entries, 50_000);
177    /// assert!(!config.enabled); // Disabled by default
178    /// ```
179    #[must_use]
180    pub const fn with_max_entries(max_entries: usize) -> Self {
181        Self {
182            enabled: false, // Consistent with new default
183            max_entries,
184            ttl_seconds: 86_400,
185            cache_list_queries: true,
186        }
187    }
188
189    /// Create cache configuration with custom TTL.
190    ///
191    /// Uses default values for other fields (**enabled=false**, 10,000 entries).
192    ///
193    /// # Arguments
194    ///
195    /// * `ttl_seconds` - Time-to-live in seconds
196    ///
197    /// # Example
198    ///
199    /// ```rust
200    /// use fraiseql_core::cache::CacheConfig;
201    ///
202    /// let config = CacheConfig::with_ttl(3_600);  // 1 hour
203    /// assert_eq!(config.ttl_seconds, 3_600);
204    /// assert!(!config.enabled); // Disabled by default
205    /// ```
206    #[must_use]
207    pub const fn with_ttl(ttl_seconds: u64) -> Self {
208        Self {
209            enabled: false, // Consistent with new default
210            max_entries: 10_000,
211            ttl_seconds,
212            cache_list_queries: true,
213        }
214    }
215
216    /// Create cache configuration with caching **enabled**.
217    ///
218    /// Use this method when you explicitly need caching (e.g., federation,
219    /// expensive computations). Most FraiseQL deployments don't need this.
220    ///
221    /// # Example
222    ///
223    /// ```rust
224    /// use fraiseql_core::cache::CacheConfig;
225    ///
226    /// let config = CacheConfig::enabled();
227    /// assert!(config.enabled);
228    /// assert_eq!(config.max_entries, 10_000);
229    /// ```
230    #[must_use]
231    pub const fn enabled() -> Self {
232        Self {
233            enabled:            true,
234            max_entries:        10_000,
235            ttl_seconds:        86_400,
236            cache_list_queries: true,
237        }
238    }
239
240    /// Create cache configuration with caching disabled.
241    ///
242    /// This is now the **default behavior**. Use this method for explicit clarity
243    /// or to override a previously enabled configuration.
244    ///
245    /// # Example
246    ///
247    /// ```rust
248    /// use fraiseql_core::cache::CacheConfig;
249    ///
250    /// let config = CacheConfig::disabled();
251    /// assert!(!config.enabled);
252    /// ```
253    #[must_use]
254    pub const fn disabled() -> Self {
255        Self {
256            enabled:            false,
257            max_entries:        10_000,
258            ttl_seconds:        86_400,
259            cache_list_queries: true,
260        }
261    }
262
263    /// Estimate memory usage in bytes for this configuration.
264    ///
265    /// This is a rough estimate assuming average entry size of 10 KB.
266    /// Actual memory usage will vary based on query result sizes.
267    ///
268    /// # Returns
269    ///
270    /// Estimated memory usage in bytes
271    ///
272    /// # Example
273    ///
274    /// ```rust
275    /// use fraiseql_core::cache::CacheConfig;
276    ///
277    /// let config = CacheConfig::default();
278    /// let estimated_bytes = config.estimated_memory_bytes();
279    /// println!("Estimated memory: {} MB", estimated_bytes / 1_000_000);
280    /// ```
281    #[must_use]
282    pub const fn estimated_memory_bytes(&self) -> usize {
283        // Rough estimate: 10 KB per entry
284        const AVG_ENTRY_SIZE_BYTES: usize = 10_000;
285        self.max_entries * AVG_ENTRY_SIZE_BYTES
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_default_config() {
295        let config = CacheConfig::default();
296        assert!(!config.enabled); // Disabled by default as of rc.12
297        assert_eq!(config.max_entries, 10_000);
298        assert_eq!(config.ttl_seconds, 86_400);
299        assert!(config.cache_list_queries);
300    }
301
302    #[test]
303    fn test_with_max_entries() {
304        let config = CacheConfig::with_max_entries(50_000);
305        assert_eq!(config.max_entries, 50_000);
306        assert!(!config.enabled); // Disabled by default as of rc.12
307        assert_eq!(config.ttl_seconds, 86_400);
308    }
309
310    #[test]
311    fn test_with_ttl() {
312        let config = CacheConfig::with_ttl(3_600);
313        assert_eq!(config.ttl_seconds, 3_600);
314        assert!(!config.enabled); // Disabled by default as of rc.12
315        assert_eq!(config.max_entries, 10_000);
316    }
317
318    #[test]
319    fn test_enabled() {
320        let config = CacheConfig::enabled();
321        assert!(config.enabled);
322        assert_eq!(config.max_entries, 10_000);
323        assert_eq!(config.ttl_seconds, 86_400);
324    }
325
326    #[test]
327    fn test_disabled() {
328        let config = CacheConfig::disabled();
329        assert!(!config.enabled);
330    }
331
332    #[test]
333    fn test_estimated_memory() {
334        let config = CacheConfig::with_max_entries(10_000);
335        let estimated = config.estimated_memory_bytes();
336        // Should be roughly 100 MB (10,000 * 10 KB)
337        assert_eq!(estimated, 100_000_000);
338    }
339
340    #[test]
341    fn test_serialization() {
342        let config = CacheConfig::default();
343        let json = serde_json::to_string(&config).unwrap();
344        let deserialized: CacheConfig = serde_json::from_str(&json).unwrap();
345
346        assert_eq!(config.enabled, deserialized.enabled);
347        assert_eq!(config.max_entries, deserialized.max_entries);
348        assert_eq!(config.ttl_seconds, deserialized.ttl_seconds);
349    }
350}