1use std::collections::VecDeque;
52use std::time::Duration;
53
54use thiserror::Error;
55
56#[derive(Debug, Error)]
60pub enum AdaptiveError {
61 #[error("invalid adaptive config: {0}")]
63 InvalidConfig(String),
64}
65
66#[derive(Debug, Clone)]
70pub struct AdaptiveConfig {
71 pub target_hit_rate: f64,
73 pub tolerance: f64,
78 pub adjustment_interval: u64,
80 pub min_capacity: usize,
82 pub max_capacity: usize,
84 pub growth_factor: f64,
86 pub shrink_factor: f64,
88 pub ttl_extension: Duration,
90 pub ttl_reduction: Duration,
92 pub window_size: usize,
94}
95
96impl Default for AdaptiveConfig {
97 fn default() -> Self {
98 Self {
99 target_hit_rate: 0.80,
100 tolerance: 0.05,
101 adjustment_interval: 100,
102 min_capacity: 16,
103 max_capacity: 65536,
104 growth_factor: 1.5,
105 shrink_factor: 0.75,
106 ttl_extension: Duration::from_secs(30),
107 ttl_reduction: Duration::from_secs(10),
108 window_size: 200,
109 }
110 }
111}
112
113impl AdaptiveConfig {
114 pub fn validate(&self) -> Result<(), AdaptiveError> {
117 if self.target_hit_rate <= 0.0 || self.target_hit_rate > 1.0 {
118 return Err(AdaptiveError::InvalidConfig(
119 "target_hit_rate must be in (0.0, 1.0]".into(),
120 ));
121 }
122 if self.tolerance < 0.0 || self.tolerance >= 1.0 {
123 return Err(AdaptiveError::InvalidConfig(
124 "tolerance must be in [0.0, 1.0)".into(),
125 ));
126 }
127 if self.adjustment_interval == 0 {
128 return Err(AdaptiveError::InvalidConfig(
129 "adjustment_interval must be > 0".into(),
130 ));
131 }
132 if self.min_capacity == 0 {
133 return Err(AdaptiveError::InvalidConfig(
134 "min_capacity must be > 0".into(),
135 ));
136 }
137 if self.max_capacity < self.min_capacity {
138 return Err(AdaptiveError::InvalidConfig(
139 "max_capacity must be >= min_capacity".into(),
140 ));
141 }
142 if self.growth_factor <= 1.0 {
143 return Err(AdaptiveError::InvalidConfig(
144 "growth_factor must be > 1.0".into(),
145 ));
146 }
147 if self.shrink_factor <= 0.0 || self.shrink_factor >= 1.0 {
148 return Err(AdaptiveError::InvalidConfig(
149 "shrink_factor must be in (0.0, 1.0)".into(),
150 ));
151 }
152 if self.window_size == 0 {
153 return Err(AdaptiveError::InvalidConfig(
154 "window_size must be > 0".into(),
155 ));
156 }
157 Ok(())
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165enum Observation {
166 Hit,
167 Miss,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum AdjustmentKind {
175 Grow,
177 Shrink,
179 NoChange,
181}
182
183#[derive(Debug, Clone)]
185pub struct Adjustment {
186 pub kind: AdjustmentKind,
188 pub recommended_capacity: usize,
190 pub ttl_extend: Duration,
196 pub ttl_reduce: Duration,
198 pub observed_hit_rate: f64,
200}
201
202struct RollingWindow {
207 buffer: VecDeque<Observation>,
208 capacity: usize,
209 hits_in_window: u64,
210}
211
212impl RollingWindow {
213 fn new(capacity: usize) -> Self {
214 Self {
215 buffer: VecDeque::with_capacity(capacity),
216 capacity: capacity.max(1),
217 hits_in_window: 0,
218 }
219 }
220
221 fn push(&mut self, obs: Observation) {
222 if self.buffer.len() == self.capacity {
223 if let Some(old) = self.buffer.pop_front() {
224 if old == Observation::Hit {
225 self.hits_in_window = self.hits_in_window.saturating_sub(1);
226 }
227 }
228 }
229 if obs == Observation::Hit {
230 self.hits_in_window += 1;
231 }
232 self.buffer.push_back(obs);
233 }
234
235 fn hit_rate(&self) -> f64 {
236 if self.buffer.is_empty() {
237 return 0.0;
238 }
239 self.hits_in_window as f64 / self.buffer.len() as f64
240 }
241
242 fn len(&self) -> usize {
243 self.buffer.len()
244 }
245
246 fn clear(&mut self) {
247 self.buffer.clear();
248 self.hits_in_window = 0;
249 }
250}
251
252pub struct AdaptivePolicy {
260 config: AdaptiveConfig,
261 window: RollingWindow,
262 current_capacity: usize,
264 ops_since_eval: u64,
266 total_hits: u64,
268 total_misses: u64,
270 adjustments_made: u64,
272 history: Vec<AdjustmentRecord>,
274 max_history: usize,
276}
277
278#[derive(Debug, Clone)]
280pub struct AdjustmentRecord {
281 pub adjustment: Adjustment,
283 pub at_operation: u64,
285}
286
287impl AdaptivePolicy {
288 pub fn new(config: AdaptiveConfig) -> Result<AdaptivePolicy, AdaptiveError> {
292 config.validate()?;
293 let initial_capacity =
294 config.min_capacity + (config.max_capacity - config.min_capacity) / 2;
295 let window = RollingWindow::new(config.window_size);
296 Ok(Self {
297 config,
298 window,
299 current_capacity: initial_capacity,
300 ops_since_eval: 0,
301 total_hits: 0,
302 total_misses: 0,
303 adjustments_made: 0,
304 history: Vec::new(),
305 max_history: 64,
306 })
307 }
308
309 pub fn with_initial_capacity(
311 config: AdaptiveConfig,
312 initial_capacity: usize,
313 ) -> Result<AdaptivePolicy, AdaptiveError> {
314 config.validate()?;
315 let clamped = initial_capacity
316 .max(config.min_capacity)
317 .min(config.max_capacity);
318 let window = RollingWindow::new(config.window_size);
319 Ok(Self {
320 config,
321 window,
322 current_capacity: clamped,
323 ops_since_eval: 0,
324 total_hits: 0,
325 total_misses: 0,
326 adjustments_made: 0,
327 history: Vec::new(),
328 max_history: 64,
329 })
330 }
331
332 pub fn record_hit(&mut self) -> Option<Adjustment> {
339 self.total_hits += 1;
340 self.window.push(Observation::Hit);
341 self.ops_since_eval += 1;
342 self.maybe_evaluate()
343 }
344
345 pub fn record_miss(&mut self) -> Option<Adjustment> {
350 self.total_misses += 1;
351 self.window.push(Observation::Miss);
352 self.ops_since_eval += 1;
353 self.maybe_evaluate()
354 }
355
356 pub fn evaluate_now(&mut self) -> Adjustment {
358 self.ops_since_eval = 0;
359 self.compute_adjustment()
360 }
361
362 pub fn rolling_hit_rate(&self) -> f64 {
366 self.window.hit_rate()
367 }
368
369 pub fn total_hits(&self) -> u64 {
371 self.total_hits
372 }
373
374 pub fn total_misses(&self) -> u64 {
376 self.total_misses
377 }
378
379 pub fn lifetime_hit_rate(&self) -> f64 {
381 let total = self.total_hits + self.total_misses;
382 if total == 0 {
383 0.0
384 } else {
385 self.total_hits as f64 / total as f64
386 }
387 }
388
389 pub fn current_capacity(&self) -> usize {
391 self.current_capacity
392 }
393
394 pub fn adjustments_made(&self) -> u64 {
396 self.adjustments_made
397 }
398
399 pub fn history(&self) -> &[AdjustmentRecord] {
401 &self.history
402 }
403
404 pub fn config(&self) -> &AdaptiveConfig {
406 &self.config
407 }
408
409 pub fn window_fill(&self) -> usize {
411 self.window.len()
412 }
413
414 pub fn reset(&mut self) {
419 self.window.clear();
420 self.ops_since_eval = 0;
421 self.total_hits = 0;
422 self.total_misses = 0;
423 self.adjustments_made = 0;
424 self.history.clear();
425 self.current_capacity =
426 self.config.min_capacity + (self.config.max_capacity - self.config.min_capacity) / 2;
427 }
428
429 fn maybe_evaluate(&mut self) -> Option<Adjustment> {
432 if self.ops_since_eval >= self.config.adjustment_interval {
433 self.ops_since_eval = 0;
434 let adj = self.compute_adjustment();
435 Some(adj)
436 } else {
437 None
438 }
439 }
440
441 fn compute_adjustment(&mut self) -> Adjustment {
442 let rate = self.window.hit_rate();
443 let target = self.config.target_hit_rate;
444 let tol = self.config.tolerance;
445
446 let (kind, new_cap, ttl_ext, ttl_red) = if rate < target - tol {
447 let raw = (self.current_capacity as f64 * self.config.growth_factor).ceil() as usize;
449 let capped = raw.min(self.config.max_capacity);
450 (
451 AdjustmentKind::Grow,
452 capped,
453 self.config.ttl_extension,
454 Duration::ZERO,
455 )
456 } else if rate > target + tol {
457 let raw = (self.current_capacity as f64 * self.config.shrink_factor).floor() as usize;
459 let floored = raw.max(self.config.min_capacity);
460 (
461 AdjustmentKind::Shrink,
462 floored,
463 Duration::ZERO,
464 self.config.ttl_reduction,
465 )
466 } else {
467 (
469 AdjustmentKind::NoChange,
470 self.current_capacity,
471 Duration::ZERO,
472 Duration::ZERO,
473 )
474 };
475
476 self.current_capacity = new_cap;
477 if kind != AdjustmentKind::NoChange {
478 self.adjustments_made += 1;
479 }
480
481 let adj = Adjustment {
482 kind,
483 recommended_capacity: new_cap,
484 ttl_extend: ttl_ext,
485 ttl_reduce: ttl_red,
486 observed_hit_rate: rate,
487 };
488
489 let total_ops = self.total_hits + self.total_misses;
491 self.history.push(AdjustmentRecord {
492 adjustment: adj.clone(),
493 at_operation: total_ops,
494 });
495 if self.history.len() > self.max_history {
496 self.history.remove(0);
497 }
498
499 adj
500 }
501}
502
503#[cfg(test)]
506mod tests {
507 use super::*;
508
509 fn default_config() -> AdaptiveConfig {
510 AdaptiveConfig {
511 target_hit_rate: 0.80,
512 tolerance: 0.05,
513 adjustment_interval: 10,
514 min_capacity: 8,
515 max_capacity: 256,
516 growth_factor: 2.0,
517 shrink_factor: 0.5,
518 ttl_extension: Duration::from_secs(10),
519 ttl_reduction: Duration::from_secs(5),
520 window_size: 50,
521 }
522 }
523
524 #[test]
526 fn test_valid_config_creates_policy() {
527 let cfg = default_config();
528 let policy = AdaptivePolicy::new(cfg);
529 assert!(policy.is_ok());
530 }
531
532 #[test]
534 fn test_invalid_target_hit_rate() {
535 let mut cfg = default_config();
536 cfg.target_hit_rate = 0.0;
537 assert!(AdaptivePolicy::new(cfg.clone()).is_err());
538
539 cfg.target_hit_rate = 1.5;
540 assert!(AdaptivePolicy::new(cfg).is_err());
541 }
542
543 #[test]
545 fn test_invalid_growth_factor() {
546 let mut cfg = default_config();
547 cfg.growth_factor = 0.5;
548 assert!(AdaptivePolicy::new(cfg).is_err());
549 }
550
551 #[test]
553 fn test_invalid_shrink_factor() {
554 let mut cfg = default_config();
555 cfg.shrink_factor = 1.0;
556 assert!(AdaptivePolicy::new(cfg.clone()).is_err());
557
558 cfg.shrink_factor = 0.0;
559 assert!(AdaptivePolicy::new(cfg).is_err());
560 }
561
562 #[test]
564 fn test_all_hits_triggers_shrink() {
565 let cfg = default_config();
566 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
567 let initial_cap = policy.current_capacity();
568
569 let mut adj = None;
571 for _ in 0..10 {
572 adj = policy.record_hit();
573 }
574 let adjustment = adj.expect("should trigger after 10 ops");
575 assert_eq!(adjustment.kind, AdjustmentKind::Shrink);
576 assert!(
577 policy.current_capacity() < initial_cap,
578 "capacity should have decreased"
579 );
580 }
581
582 #[test]
584 fn test_all_misses_triggers_grow() {
585 let cfg = default_config();
586 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
587 let initial_cap = policy.current_capacity();
588
589 let mut adj = None;
590 for _ in 0..10 {
591 adj = policy.record_miss();
592 }
593 let adjustment = adj.expect("should trigger after 10 ops");
594 assert_eq!(adjustment.kind, AdjustmentKind::Grow);
595 assert!(
596 policy.current_capacity() > initial_cap,
597 "capacity should have increased"
598 );
599 }
600
601 #[test]
603 fn test_within_tolerance_no_change() {
604 let cfg = default_config(); let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
606
607 for _ in 0..8 {
609 let _ = policy.record_hit();
610 }
611 let mut adj = None;
612 for _ in 0..2 {
613 adj = policy.record_miss();
614 }
615 let adjustment = adj.expect("should trigger after 10 ops");
616 assert_eq!(adjustment.kind, AdjustmentKind::NoChange);
617 }
618
619 #[test]
621 fn test_capacity_capped_at_max() {
622 let mut cfg = default_config();
623 cfg.max_capacity = 300;
624 cfg.growth_factor = 100.0; let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
626
627 for _ in 0..10 {
629 let _ = policy.record_miss();
630 }
631 assert!(
632 policy.current_capacity() <= 300,
633 "capacity {} should not exceed max 300",
634 policy.current_capacity()
635 );
636 }
637
638 #[test]
640 fn test_capacity_floored_at_min() {
641 let mut cfg = default_config();
642 cfg.min_capacity = 4;
643 cfg.shrink_factor = 0.01; let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
645
646 for _ in 0..10 {
648 let _ = policy.record_hit();
649 }
650 assert!(
651 policy.current_capacity() >= 4,
652 "capacity {} should not drop below min 4",
653 policy.current_capacity()
654 );
655 }
656
657 #[test]
659 fn test_rolling_window_discards_old() {
660 let mut cfg = default_config();
661 cfg.window_size = 10;
662 cfg.adjustment_interval = 10;
663 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
664
665 for _ in 0..10 {
667 let _ = policy.record_miss();
668 }
669 for _ in 0..10 {
671 let _ = policy.record_hit();
672 }
673 let rate = policy.rolling_hit_rate();
675 assert!(
676 (rate - 1.0).abs() < 1e-9,
677 "rolling hit rate should be 1.0, got {rate}"
678 );
679 }
680
681 #[test]
683 fn test_evaluate_now() {
684 let cfg = default_config();
685 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
686
687 for _ in 0..3 {
689 let _ = policy.record_hit();
690 }
691 let adj = policy.evaluate_now();
693 assert_eq!(adj.kind, AdjustmentKind::Shrink);
695 }
696
697 #[test]
699 fn test_reset_clears_state() {
700 let cfg = default_config();
701 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
702
703 for _ in 0..20 {
704 let _ = policy.record_hit();
705 }
706 assert!(policy.total_hits() > 0);
707 assert!(!policy.history().is_empty());
708
709 policy.reset();
710 assert_eq!(policy.total_hits(), 0);
711 assert_eq!(policy.total_misses(), 0);
712 assert_eq!(policy.adjustments_made(), 0);
713 assert!(policy.history().is_empty());
714 assert_eq!(policy.window_fill(), 0);
715 }
716
717 #[test]
719 fn test_ttl_extension_on_grow() {
720 let cfg = default_config();
721 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
722
723 let mut adj = None;
724 for _ in 0..10 {
725 adj = policy.record_miss();
726 }
727 let adjustment = adj.expect("should trigger");
728 assert_eq!(adjustment.kind, AdjustmentKind::Grow);
729 assert_eq!(adjustment.ttl_extend, Duration::from_secs(10));
730 assert_eq!(adjustment.ttl_reduce, Duration::ZERO);
731 }
732
733 #[test]
735 fn test_ttl_reduction_on_shrink() {
736 let cfg = default_config();
737 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
738
739 let mut adj = None;
740 for _ in 0..10 {
741 adj = policy.record_hit();
742 }
743 let adjustment = adj.expect("should trigger");
744 assert_eq!(adjustment.kind, AdjustmentKind::Shrink);
745 assert_eq!(adjustment.ttl_extend, Duration::ZERO);
746 assert_eq!(adjustment.ttl_reduce, Duration::from_secs(5));
747 }
748
749 #[test]
751 fn test_lifetime_hit_rate() {
752 let cfg = default_config();
753 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
754
755 for _ in 0..7 {
756 let _ = policy.record_hit();
757 }
758 for _ in 0..3 {
759 let _ = policy.record_miss();
760 }
761 let rate = policy.lifetime_hit_rate();
762 assert!((rate - 0.7).abs() < 1e-9, "expected 0.7, got {rate}");
763 }
764
765 #[test]
767 fn test_history_records() {
768 let cfg = default_config(); let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
770
771 for _ in 0..20 {
773 let _ = policy.record_hit();
774 }
775 assert!(
776 policy.history().len() >= 2,
777 "expected at least 2 history records, got {}",
778 policy.history().len()
779 );
780 }
781
782 #[test]
784 fn test_with_initial_capacity_clamps() {
785 let cfg = default_config(); let p1 = AdaptivePolicy::with_initial_capacity(cfg.clone(), 2).expect("valid");
787 assert_eq!(p1.current_capacity(), 8, "should clamp to min");
788
789 let p2 = AdaptivePolicy::with_initial_capacity(cfg, 9999).expect("valid");
790 assert_eq!(p2.current_capacity(), 256, "should clamp to max");
791 }
792
793 #[test]
795 fn test_min_equals_max_capacity() {
796 let mut cfg = default_config();
797 cfg.min_capacity = 64;
798 cfg.max_capacity = 64;
799 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
800
801 for _ in 0..10 {
803 let _ = policy.record_miss();
804 }
805 assert_eq!(policy.current_capacity(), 64);
806
807 for _ in 0..10 {
809 let _ = policy.record_hit();
810 }
811 assert_eq!(policy.current_capacity(), 64);
812 }
813
814 #[test]
816 fn test_zero_adjustment_interval_rejected() {
817 let mut cfg = default_config();
818 cfg.adjustment_interval = 0;
819 assert!(AdaptivePolicy::new(cfg).is_err());
820 }
821
822 #[test]
824 fn test_zero_window_size_rejected() {
825 let mut cfg = default_config();
826 cfg.window_size = 0;
827 assert!(AdaptivePolicy::new(cfg).is_err());
828 }
829
830 #[test]
832 fn test_successive_grow_monotonic() {
833 let mut cfg = default_config();
834 cfg.max_capacity = 100_000;
835 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
836
837 let mut prev_cap = policy.current_capacity();
838 for round in 0..5 {
839 for _ in 0..10 {
840 let _ = policy.record_miss();
841 }
842 let cap = policy.current_capacity();
843 assert!(
844 cap >= prev_cap,
845 "round {round}: capacity should not decrease on grow ({prev_cap} → {cap})"
846 );
847 prev_cap = cap;
848 }
849 }
850
851 #[test]
853 fn test_observed_hit_rate_in_adjustment() {
854 let cfg = default_config();
855 let mut policy = AdaptivePolicy::new(cfg).expect("valid config");
856
857 for _ in 0..5 {
858 let _ = policy.record_hit();
859 }
860 for _ in 0..5 {
861 let _ = policy.record_miss();
862 }
863 let last_hist = policy.history().last().expect("should have history");
865 let rate = last_hist.adjustment.observed_hit_rate;
866 assert!(
867 (rate - 0.5).abs() < 1e-9,
868 "expected 0.5 hit rate, got {rate}"
869 );
870 }
871}