Skip to main content

heliosdb_proxy/cache/
config.rs

1//! Cache Configuration
2//!
3//! Configuration structures for the multi-tier query cache.
4
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::time::Duration;
8
9/// Main cache configuration
10#[derive(Debug, Clone)]
11pub struct CacheConfig {
12    /// Enable/disable cache globally
13    pub enabled: bool,
14
15    /// L1 hot cache configuration
16    pub l1: L1Config,
17
18    /// L2 warm cache configuration
19    pub l2: L2Config,
20
21    /// L3 semantic cache configuration
22    pub l3: L3Config,
23
24    /// Cache invalidation configuration
25    pub invalidation: InvalidationConfig,
26
27    /// Default TTL for cached results
28    pub default_ttl: Duration,
29
30    /// Maximum result size to cache (bytes)
31    pub max_result_size: usize,
32
33    /// Table-specific configurations
34    pub table_configs: HashMap<String, TableCacheConfig>,
35
36    /// Excluded tables (never cache)
37    pub excluded_tables: Vec<String>,
38}
39
40impl Default for CacheConfig {
41    fn default() -> Self {
42        Self {
43            enabled: true,
44            l1: L1Config::default(),
45            l2: L2Config::default(),
46            l3: L3Config::default(),
47            invalidation: InvalidationConfig::default(),
48            default_ttl: Duration::from_secs(300), // 5 minutes
49            max_result_size: 10 * 1024 * 1024,     // 10 MB
50            table_configs: HashMap::new(),
51            excluded_tables: Vec::new(),
52        }
53    }
54}
55
56impl CacheConfig {
57    /// Create a minimal configuration (L1 only)
58    pub fn minimal() -> Self {
59        Self {
60            enabled: true,
61            l1: L1Config::default(),
62            l2: L2Config { enabled: false, ..Default::default() },
63            l3: L3Config { enabled: false, ..Default::default() },
64            ..Default::default()
65        }
66    }
67
68    /// Create a configuration optimized for high-throughput reads
69    pub fn high_throughput() -> Self {
70        Self {
71            enabled: true,
72            l1: L1Config {
73                size: 2000,
74                ttl: Duration::from_secs(60),
75                ..Default::default()
76            },
77            l2: L2Config {
78                size_mb: 1024,
79                ttl: Duration::from_secs(600),
80                ..Default::default()
81            },
82            l3: L3Config { enabled: false, ..Default::default() },
83            default_ttl: Duration::from_secs(300),
84            ..Default::default()
85        }
86    }
87
88    /// Create a configuration optimized for AI/RAG workloads
89    pub fn ai_workload() -> Self {
90        Self {
91            enabled: true,
92            l1: L1Config::default(),
93            l2: L2Config::default(),
94            l3: L3Config {
95                enabled: true,
96                similarity_threshold: 0.90,
97                max_entries: 10000,
98                ..Default::default()
99            },
100            ..Default::default()
101        }
102    }
103
104    /// Validate configuration
105    pub fn validate(&self) -> Result<(), String> {
106        if self.l1.size == 0 && self.l1.enabled {
107            return Err("L1 cache size cannot be 0 when enabled".to_string());
108        }
109
110        if self.l2.size_mb == 0 && self.l2.enabled {
111            return Err("L2 cache size cannot be 0 when enabled".to_string());
112        }
113
114        if self.l3.similarity_threshold < 0.0 || self.l3.similarity_threshold > 1.0 {
115            return Err("L3 similarity threshold must be between 0.0 and 1.0".to_string());
116        }
117
118        Ok(())
119    }
120}
121
122/// L1 hot cache configuration (per-connection)
123#[derive(Debug, Clone)]
124pub struct L1Config {
125    /// Enable L1 cache
126    pub enabled: bool,
127
128    /// Maximum entries per connection
129    pub size: usize,
130
131    /// Time-to-live for cached entries
132    pub ttl: Duration,
133}
134
135impl Default for L1Config {
136    fn default() -> Self {
137        Self {
138            enabled: true,
139            size: 500,
140            ttl: Duration::from_secs(30),
141        }
142    }
143}
144
145/// L2 warm cache configuration (shared)
146#[derive(Debug, Clone)]
147pub struct L2Config {
148    /// Enable L2 cache
149    pub enabled: bool,
150
151    /// Maximum cache size in MB
152    pub size_mb: usize,
153
154    /// Time-to-live for cached entries
155    pub ttl: Duration,
156
157    /// Enable query normalization
158    pub normalize_queries: bool,
159
160    /// Storage backend
161    pub storage: StorageBackend,
162
163    /// Memory-mapped file path (for mmap backend)
164    pub mmap_path: Option<PathBuf>,
165}
166
167impl Default for L2Config {
168    fn default() -> Self {
169        Self {
170            enabled: true,
171            size_mb: 256,
172            ttl: Duration::from_secs(300),
173            normalize_queries: true,
174            storage: StorageBackend::Memory,
175            mmap_path: None,
176        }
177    }
178}
179
180/// Storage backend for L2 cache
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum StorageBackend {
183    /// In-process memory (lost on restart)
184    Memory,
185    /// Memory-mapped file (survives restarts)
186    Mmap,
187}
188
189impl Default for StorageBackend {
190    fn default() -> Self {
191        Self::Memory
192    }
193}
194
195/// L3 semantic cache configuration
196#[derive(Debug, Clone)]
197pub struct L3Config {
198    /// Enable L3 semantic cache
199    pub enabled: bool,
200
201    /// Cosine similarity threshold for cache hits
202    pub similarity_threshold: f32,
203
204    /// Maximum entries in semantic cache
205    pub max_entries: usize,
206
207    /// Time-to-live for semantic cache entries
208    pub ttl: Duration,
209
210    /// Ollama endpoint for embeddings
211    pub embedding_endpoint: String,
212
213    /// Embedding model name
214    pub embedding_model: String,
215
216    /// Embedding dimension
217    pub embedding_dim: usize,
218}
219
220impl Default for L3Config {
221    fn default() -> Self {
222        Self {
223            enabled: false, // Disabled by default (requires Ollama)
224            similarity_threshold: 0.92,
225            max_entries: 5000,
226            ttl: Duration::from_secs(3600),
227            embedding_endpoint: "http://localhost:11434".to_string(),
228            embedding_model: "all-minilm".to_string(),
229            embedding_dim: 384, // all-MiniLM-L6-v2 dimension
230        }
231    }
232}
233
234/// Cache invalidation configuration
235#[derive(Debug, Clone)]
236pub struct InvalidationConfig {
237    /// Invalidation mode
238    pub mode: super::InvalidationMode,
239
240    /// Subscribe to WAL for automatic invalidation
241    pub wal_subscribe: bool,
242
243    /// Fallback TTL when WAL is unavailable
244    pub ttl_fallback: Duration,
245}
246
247impl Default for InvalidationConfig {
248    fn default() -> Self {
249        Self {
250            mode: super::InvalidationMode::Wal,
251            wal_subscribe: true,
252            ttl_fallback: Duration::from_secs(60),
253        }
254    }
255}
256
257/// Table-specific cache configuration
258#[derive(Debug, Clone)]
259pub struct TableCacheConfig {
260    /// Time-to-live for this table
261    pub ttl: Duration,
262
263    /// Exclude this table from caching
264    pub exclude: bool,
265
266    /// Columns to exclude from cached results
267    pub exclude_columns: Vec<String>,
268}
269
270impl Default for TableCacheConfig {
271    fn default() -> Self {
272        Self {
273            ttl: Duration::from_secs(300),
274            exclude: false,
275            exclude_columns: Vec::new(),
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_default_config() {
286        let config = CacheConfig::default();
287        assert!(config.enabled);
288        assert!(config.l1.enabled);
289        assert!(config.l2.enabled);
290        assert!(!config.l3.enabled); // Disabled by default
291        assert_eq!(config.l1.size, 500);
292        assert_eq!(config.l2.size_mb, 256);
293    }
294
295    #[test]
296    fn test_minimal_config() {
297        let config = CacheConfig::minimal();
298        assert!(config.l1.enabled);
299        assert!(!config.l2.enabled);
300        assert!(!config.l3.enabled);
301    }
302
303    #[test]
304    fn test_high_throughput_config() {
305        let config = CacheConfig::high_throughput();
306        assert_eq!(config.l1.size, 2000);
307        assert_eq!(config.l2.size_mb, 1024);
308        assert!(!config.l3.enabled);
309    }
310
311    #[test]
312    fn test_ai_workload_config() {
313        let config = CacheConfig::ai_workload();
314        assert!(config.l3.enabled);
315        assert_eq!(config.l3.similarity_threshold, 0.90);
316    }
317
318    #[test]
319    fn test_validation() {
320        let config = CacheConfig::default();
321        assert!(config.validate().is_ok());
322
323        let mut invalid = CacheConfig::default();
324        invalid.l1.size = 0;
325        assert!(invalid.validate().is_err());
326
327        let mut invalid2 = CacheConfig::default();
328        invalid2.l3.similarity_threshold = 1.5;
329        assert!(invalid2.validate().is_err());
330    }
331
332    #[test]
333    fn test_storage_backend() {
334        assert_eq!(StorageBackend::default(), StorageBackend::Memory);
335    }
336}