do_memory_storage_turso/cache/
ttl_config.rs1use std::time::Duration;
7
8pub const DEFAULT_BASE_TTL: Duration = Duration::from_secs(300);
10
11pub const DEFAULT_MIN_TTL: Duration = Duration::from_secs(60);
13
14pub const DEFAULT_MAX_TTL: Duration = Duration::from_secs(3600);
16
17pub const DEFAULT_HOT_THRESHOLD: u64 = 10;
19
20pub const DEFAULT_COLD_THRESHOLD: u64 = 2;
22
23pub const DEFAULT_ADAPTATION_RATE: f64 = 0.25;
25
26pub const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
28
29#[derive(Debug, Clone)]
31pub struct TTLConfig {
32 pub base_ttl: Duration,
34
35 pub min_ttl: Duration,
37
38 pub max_ttl: Duration,
40
41 pub hot_threshold: u64,
43
44 pub cold_threshold: u64,
46
47 pub adaptation_rate: f64,
49
50 pub enable_background_cleanup: bool,
52
53 pub cleanup_interval: Duration,
55
56 pub max_entries: usize,
58
59 pub memory_pressure_threshold: f64,
61
62 pub enable_adaptive_ttl: bool,
64
65 pub access_window_secs: u64,
67}
68
69impl Default for TTLConfig {
70 fn default() -> Self {
71 Self {
72 base_ttl: DEFAULT_BASE_TTL,
73 min_ttl: DEFAULT_MIN_TTL,
74 max_ttl: DEFAULT_MAX_TTL,
75 hot_threshold: DEFAULT_HOT_THRESHOLD,
76 cold_threshold: DEFAULT_COLD_THRESHOLD,
77 adaptation_rate: DEFAULT_ADAPTATION_RATE,
78 enable_background_cleanup: true,
79 cleanup_interval: DEFAULT_CLEANUP_INTERVAL,
80 max_entries: 10_000,
81 memory_pressure_threshold: 0.8,
82 enable_adaptive_ttl: true,
83 access_window_secs: 300, }
85 }
86}
87
88impl TTLConfig {
89 pub fn new() -> Self {
91 Self::default()
92 }
93
94 pub fn high_hit_rate() -> Self {
96 Self {
97 base_ttl: Duration::from_secs(600), min_ttl: Duration::from_secs(120), max_ttl: Duration::from_secs(7200), hot_threshold: 5, cold_threshold: 1,
102 adaptation_rate: 0.35, enable_background_cleanup: true,
104 cleanup_interval: Duration::from_secs(30),
105 max_entries: 20_000,
106 memory_pressure_threshold: 0.85,
107 enable_adaptive_ttl: true,
108 access_window_secs: 180, }
110 }
111
112 pub fn memory_constrained() -> Self {
114 Self {
115 base_ttl: Duration::from_secs(180), min_ttl: Duration::from_secs(30), max_ttl: Duration::from_secs(1800), hot_threshold: 15,
119 cold_threshold: 3,
120 adaptation_rate: 0.2,
121 enable_background_cleanup: true,
122 cleanup_interval: Duration::from_secs(15),
123 max_entries: 1_000,
124 memory_pressure_threshold: 0.6, enable_adaptive_ttl: true,
126 access_window_secs: 120, }
128 }
129
130 pub fn write_heavy() -> Self {
132 Self {
133 base_ttl: Duration::from_secs(120), min_ttl: Duration::from_secs(30), max_ttl: Duration::from_secs(600), hot_threshold: 20,
137 cold_threshold: 5,
138 adaptation_rate: 0.15, enable_background_cleanup: true,
140 cleanup_interval: Duration::from_secs(20),
141 max_entries: 5_000,
142 memory_pressure_threshold: 0.75,
143 enable_adaptive_ttl: true,
144 access_window_secs: 60, }
146 }
147
148 pub fn validate(&self) -> Result<(), TTLConfigError> {
150 if self.min_ttl > self.base_ttl {
151 return Err(TTLConfigError::InvalidBounds(
152 "min_ttl cannot be greater than base_ttl".to_string(),
153 ));
154 }
155 if self.base_ttl > self.max_ttl {
156 return Err(TTLConfigError::InvalidBounds(
157 "base_ttl cannot be greater than max_ttl".to_string(),
158 ));
159 }
160 if !(0.0..=1.0).contains(&self.adaptation_rate) {
161 return Err(TTLConfigError::InvalidAdaptationRate(self.adaptation_rate));
162 }
163 if !(0.0..=1.0).contains(&self.memory_pressure_threshold) {
164 return Err(TTLConfigError::InvalidThreshold(
165 self.memory_pressure_threshold,
166 ));
167 }
168 if self.hot_threshold <= self.cold_threshold {
169 return Err(TTLConfigError::InvalidThresholds {
170 hot: self.hot_threshold,
171 cold: self.cold_threshold,
172 });
173 }
174 if self.max_entries == 0 {
175 return Err(TTLConfigError::InvalidMaxEntries);
176 }
177 Ok(())
178 }
179
180 pub fn calculate_ttl(&self, current_ttl: Duration, access_count: u64) -> Duration {
182 if !self.enable_adaptive_ttl {
183 return self.base_ttl;
184 }
185
186 let new_ttl = if access_count >= self.hot_threshold {
187 let extension = current_ttl.mul_f64(self.adaptation_rate);
189 current_ttl + extension
190 } else if access_count <= self.cold_threshold {
191 let reduction = current_ttl.mul_f64(self.adaptation_rate);
193 current_ttl.saturating_sub(reduction)
194 } else {
195 current_ttl
196 };
197
198 new_ttl.clamp(self.min_ttl, self.max_ttl)
200 }
201
202 pub fn with_base_ttl(mut self, ttl: Duration) -> Self {
204 self.base_ttl = ttl;
205 self
206 }
207
208 pub fn with_min_ttl(mut self, ttl: Duration) -> Self {
210 self.min_ttl = ttl;
211 self
212 }
213
214 pub fn with_max_ttl(mut self, ttl: Duration) -> Self {
216 self.max_ttl = ttl;
217 self
218 }
219
220 pub fn with_hot_threshold(mut self, threshold: u64) -> Self {
222 self.hot_threshold = threshold;
223 self
224 }
225
226 pub fn with_cold_threshold(mut self, threshold: u64) -> Self {
228 self.cold_threshold = threshold;
229 self
230 }
231
232 pub fn with_adaptation_rate(mut self, rate: f64) -> Self {
234 self.adaptation_rate = rate.clamp(0.0, 1.0);
235 self
236 }
237
238 pub fn with_max_entries(mut self, max: usize) -> Self {
240 self.max_entries = max;
241 self
242 }
243}
244
245#[derive(Debug, Clone, PartialEq)]
247pub enum TTLConfigError {
248 InvalidBounds(String),
250 InvalidAdaptationRate(f64),
252 InvalidThreshold(f64),
254 InvalidThresholds { hot: u64, cold: u64 },
256 InvalidMaxEntries,
258}
259
260impl std::fmt::Display for TTLConfigError {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 match self {
263 Self::InvalidBounds(msg) => write!(f, "Invalid TTL bounds: {msg}"),
264 Self::InvalidAdaptationRate(rate) => {
265 write!(f, "Invalid adaptation rate: {rate} (must be 0.0 - 1.0)")
266 }
267 Self::InvalidThreshold(threshold) => {
268 write!(f, "Invalid threshold: {threshold} (must be 0.0 - 1.0)")
269 }
270 Self::InvalidThresholds { hot, cold } => {
271 write!(f, "Invalid thresholds: hot ({hot}) must be > cold ({cold})")
272 }
273 Self::InvalidMaxEntries => write!(f, "Invalid max entries: must be > 0"),
274 }
275 }
276}
277
278impl std::error::Error for TTLConfigError {}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_default_config() {
286 let config = TTLConfig::default();
287 assert_eq!(config.base_ttl, DEFAULT_BASE_TTL);
288 assert_eq!(config.min_ttl, DEFAULT_MIN_TTL);
289 assert_eq!(config.max_ttl, DEFAULT_MAX_TTL);
290 assert!(config.validate().is_ok());
291 }
292
293 #[test]
294 fn test_high_hit_rate_config() {
295 let config = TTLConfig::high_hit_rate();
296 assert!(config.validate().is_ok());
297 assert_eq!(config.base_ttl, Duration::from_secs(600));
298 assert!(config.max_entries > 10_000);
299 }
300
301 #[test]
302 fn test_memory_constrained_config() {
303 let config = TTLConfig::memory_constrained();
304 assert!(config.validate().is_ok());
305 assert!(config.max_entries < 2_000);
306 assert!(config.memory_pressure_threshold < 0.7);
307 }
308
309 #[test]
310 fn test_write_heavy_config() {
311 let config = TTLConfig::write_heavy();
312 assert!(config.validate().is_ok());
313 assert!(config.base_ttl < Duration::from_secs(300));
314 }
315
316 #[test]
317 fn test_invalid_bounds() {
318 let config = TTLConfig {
319 min_ttl: Duration::from_secs(600),
320 base_ttl: Duration::from_secs(300),
321 ..Default::default()
322 };
323 assert!(matches!(
324 config.validate(),
325 Err(TTLConfigError::InvalidBounds(_))
326 ));
327 }
328
329 #[test]
330 fn test_invalid_adaptation_rate() {
331 let config = TTLConfig {
332 adaptation_rate: 1.5,
333 ..Default::default()
334 };
335 assert!(matches!(
336 config.validate(),
337 Err(TTLConfigError::InvalidAdaptationRate(1.5))
338 ));
339 }
340
341 #[test]
342 fn test_invalid_thresholds() {
343 let config = TTLConfig {
344 hot_threshold: 2,
345 cold_threshold: 5,
346 ..Default::default()
347 };
348 assert!(matches!(
349 config.validate(),
350 Err(TTLConfigError::InvalidThresholds { hot: 2, cold: 5 })
351 ));
352 }
353
354 #[test]
355 fn test_calculate_ttl_hot() {
356 let config = TTLConfig::default();
357 let current = Duration::from_secs(300);
358 let new_ttl = config.calculate_ttl(current, 15); assert!(new_ttl > current);
360 assert!(new_ttl <= config.max_ttl);
361 }
362
363 #[test]
364 fn test_calculate_ttl_cold() {
365 let config = TTLConfig::default();
366 let current = Duration::from_secs(300);
367 let new_ttl = config.calculate_ttl(current, 1); assert!(new_ttl < current);
369 assert!(new_ttl >= config.min_ttl);
370 }
371
372 #[test]
373 fn test_calculate_ttl_neutral() {
374 let config = TTLConfig::default();
375 let current = Duration::from_secs(300);
376 let new_ttl = config.calculate_ttl(current, 5); assert_eq!(new_ttl, current);
378 }
379
380 #[test]
381 fn test_builder_methods() {
382 let config = TTLConfig::new()
383 .with_base_ttl(Duration::from_secs(600))
384 .with_min_ttl(Duration::from_secs(120))
385 .with_max_ttl(Duration::from_secs(7200))
386 .with_hot_threshold(20)
387 .with_cold_threshold(3)
388 .with_adaptation_rate(0.5)
389 .with_max_entries(5000);
390
391 assert_eq!(config.base_ttl, Duration::from_secs(600));
392 assert_eq!(config.min_ttl, Duration::from_secs(120));
393 assert_eq!(config.max_ttl, Duration::from_secs(7200));
394 assert_eq!(config.hot_threshold, 20);
395 assert_eq!(config.cold_threshold, 3);
396 assert_eq!(config.adaptation_rate, 0.5);
397 assert_eq!(config.max_entries, 5000);
398 assert!(config.validate().is_ok());
399 }
400
401 #[test]
402 fn test_adaptation_rate_clamping() {
403 let config = TTLConfig::new().with_adaptation_rate(1.5);
404 assert_eq!(config.adaptation_rate, 1.0);
405
406 let config = TTLConfig::new().with_adaptation_rate(-0.5);
407 assert_eq!(config.adaptation_rate, 0.0);
408 }
409}