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}