1use chrono::{DateTime, Utc};
90use rust_decimal::Decimal;
91use std::collections::HashMap;
92use std::sync::{Arc, RwLock};
93use uuid::Uuid;
94
95use crate::error::ReputationError;
96use crate::score::ReputationScore;
97use crate::tier::ReputationTier;
98
99const DEFAULT_TTL_SECONDS: i64 = 300; #[derive(Debug, Clone)]
104pub struct CachedScore {
105 pub score: ReputationScore,
106 pub cached_at: DateTime<Utc>,
107 pub ttl_seconds: i64,
108}
109
110impl CachedScore {
111 pub fn is_valid(&self) -> bool {
113 let now = Utc::now();
114 let age = now.signed_duration_since(self.cached_at).num_seconds();
115 age < self.ttl_seconds
116 }
117
118 pub fn remaining_ttl(&self) -> i64 {
120 let now = Utc::now();
121 let age = now.signed_duration_since(self.cached_at).num_seconds();
122 (self.ttl_seconds - age).max(0)
123 }
124}
125
126#[derive(Debug, Clone)]
128pub struct CachedTier {
129 pub tier: ReputationTier,
130 pub score: Decimal,
131 pub cached_at: DateTime<Utc>,
132}
133
134#[derive(Debug, Clone, Default)]
136struct HitRateMetrics {
137 score_hits: usize,
138 score_misses: usize,
139 tier_hits: usize,
140 tier_misses: usize,
141}
142
143#[derive(Debug, Clone)]
145pub struct ReputationCache {
146 scores: Arc<RwLock<HashMap<Uuid, CachedScore>>>,
147 tiers: Arc<RwLock<HashMap<Uuid, CachedTier>>>,
148 ttl_seconds: i64,
149 metrics: Arc<RwLock<HitRateMetrics>>,
150}
151
152impl Default for ReputationCache {
153 fn default() -> Self {
154 Self::new(DEFAULT_TTL_SECONDS)
155 }
156}
157
158impl ReputationCache {
159 pub fn new(ttl_seconds: i64) -> Self {
161 Self {
162 scores: Arc::new(RwLock::new(HashMap::new())),
163 tiers: Arc::new(RwLock::new(HashMap::new())),
164 ttl_seconds,
165 metrics: Arc::new(RwLock::new(HitRateMetrics::default())),
166 }
167 }
168
169 pub fn get_score(&self, user_id: Uuid) -> Option<ReputationScore> {
171 let scores = self.scores.read().ok()?;
172 let cached = scores.get(&user_id);
173
174 if let Some(cached_entry) = cached {
175 if cached_entry.is_valid() {
176 if let Ok(mut metrics) = self.metrics.write() {
178 metrics.score_hits += 1;
179 }
180 Some(cached_entry.score.clone())
181 } else {
182 drop(scores);
184 self.invalidate_score(user_id);
185 if let Ok(mut metrics) = self.metrics.write() {
186 metrics.score_misses += 1;
187 }
188 None
189 }
190 } else {
191 if let Ok(mut metrics) = self.metrics.write() {
193 metrics.score_misses += 1;
194 }
195 None
196 }
197 }
198
199 pub fn set_score(&self, user_id: Uuid, score: ReputationScore) -> Result<(), ReputationError> {
201 let mut scores = self
202 .scores
203 .write()
204 .map_err(|_| ReputationError::Validation("Cache lock poisoned".to_string()))?;
205
206 scores.insert(
207 user_id,
208 CachedScore {
209 score,
210 cached_at: Utc::now(),
211 ttl_seconds: self.ttl_seconds,
212 },
213 );
214
215 Ok(())
216 }
217
218 pub fn invalidate_score(&self, user_id: Uuid) {
220 if let Ok(mut scores) = self.scores.write() {
221 scores.remove(&user_id);
222 }
223 }
224
225 pub fn get_tier(&self, user_id: Uuid) -> Option<(ReputationTier, Decimal)> {
227 let tiers = self.tiers.read().ok()?;
228 let cached = tiers.get(&user_id);
229
230 if let Some(cached_entry) = cached {
231 if let Ok(mut metrics) = self.metrics.write() {
233 metrics.tier_hits += 1;
234 }
235 Some((cached_entry.tier, cached_entry.score))
236 } else {
237 if let Ok(mut metrics) = self.metrics.write() {
239 metrics.tier_misses += 1;
240 }
241 None
242 }
243 }
244
245 pub fn set_tier(
247 &self,
248 user_id: Uuid,
249 tier: ReputationTier,
250 score: Decimal,
251 ) -> Result<(), ReputationError> {
252 let mut tiers = self
253 .tiers
254 .write()
255 .map_err(|_| ReputationError::Validation("Cache lock poisoned".to_string()))?;
256
257 tiers.insert(
258 user_id,
259 CachedTier {
260 tier,
261 score,
262 cached_at: Utc::now(),
263 },
264 );
265
266 Ok(())
267 }
268
269 pub fn invalidate_tier(&self, user_id: Uuid) {
271 if let Ok(mut tiers) = self.tiers.write() {
272 tiers.remove(&user_id);
273 }
274 }
275
276 pub fn invalidate_user(&self, user_id: Uuid) {
278 self.invalidate_score(user_id);
279 self.invalidate_tier(user_id);
280 }
281
282 pub fn clear_scores(&self) {
284 if let Ok(mut scores) = self.scores.write() {
285 scores.clear();
286 }
287 }
288
289 pub fn clear_tiers(&self) {
291 if let Ok(mut tiers) = self.tiers.write() {
292 tiers.clear();
293 }
294 }
295
296 pub fn clear_all(&self) {
298 self.clear_scores();
299 self.clear_tiers();
300 }
301
302 pub fn cleanup_expired(&self) -> CacheCleanupStats {
304 let mut expired_scores = 0;
305
306 if let Ok(mut scores) = self.scores.write() {
307 scores.retain(|_, cached| {
308 let valid = cached.is_valid();
309 if !valid {
310 expired_scores += 1;
311 }
312 valid
313 });
314 }
315
316 CacheCleanupStats {
317 expired_scores,
318 cleaned_at: Utc::now(),
319 }
320 }
321
322 pub fn get_stats(&self) -> CacheStats {
324 let scores_count = self.scores.read().map(|s| s.len()).unwrap_or(0);
325 let tiers_count = self.tiers.read().map(|t| t.len()).unwrap_or(0);
326
327 let metrics = self.metrics.read().ok();
328 let (score_hits, score_misses, tier_hits, tier_misses) = if let Some(m) = metrics {
329 (m.score_hits, m.score_misses, m.tier_hits, m.tier_misses)
330 } else {
331 (0, 0, 0, 0)
332 };
333
334 let score_hit_rate = if score_hits + score_misses > 0 {
336 score_hits as f64 / (score_hits + score_misses) as f64
337 } else {
338 0.0
339 };
340
341 let tier_hit_rate = if tier_hits + tier_misses > 0 {
342 tier_hits as f64 / (tier_hits + tier_misses) as f64
343 } else {
344 0.0
345 };
346
347 let total_hits = score_hits + tier_hits;
348 let total_ops = score_hits + score_misses + tier_hits + tier_misses;
349 let overall_hit_rate = if total_ops > 0 {
350 total_hits as f64 / total_ops as f64
351 } else {
352 0.0
353 };
354
355 CacheStats {
356 cached_scores: scores_count,
357 cached_tiers: tiers_count,
358 ttl_seconds: self.ttl_seconds,
359 score_hits,
360 score_misses,
361 tier_hits,
362 tier_misses,
363 score_hit_rate,
364 tier_hit_rate,
365 overall_hit_rate,
366 }
367 }
368
369 pub fn reset_metrics(&self) {
371 if let Ok(mut metrics) = self.metrics.write() {
372 metrics.score_hits = 0;
373 metrics.score_misses = 0;
374 metrics.tier_hits = 0;
375 metrics.tier_misses = 0;
376 }
377 }
378}
379
380#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
382pub struct CacheStats {
383 pub cached_scores: usize,
384 pub cached_tiers: usize,
385 pub ttl_seconds: i64,
386 pub score_hits: usize,
387 pub score_misses: usize,
388 pub tier_hits: usize,
389 pub tier_misses: usize,
390 pub score_hit_rate: f64,
391 pub tier_hit_rate: f64,
392 pub overall_hit_rate: f64,
393}
394
395impl CacheStats {
396 pub fn is_healthy(&self) -> bool {
398 self.overall_hit_rate > 0.7
399 }
400
401 pub fn is_poor(&self) -> bool {
403 self.overall_hit_rate < 0.3
404 }
405
406 pub fn total_operations(&self) -> usize {
408 self.score_hits + self.score_misses + self.tier_hits + self.tier_misses
409 }
410}
411
412#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
414pub struct CacheCleanupStats {
415 pub expired_scores: usize,
416 pub cleaned_at: DateTime<Utc>,
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use rust_decimal_macros::dec;
423
424 #[test]
425 fn test_cached_score_is_valid() {
426 let score = ReputationScore {
427 user_id: Uuid::new_v4(),
428 overall_score: dec!(500),
429 tier: ReputationTier::Silver,
430 components: Default::default(),
431 };
432
433 let cached = CachedScore {
434 score,
435 cached_at: Utc::now(),
436 ttl_seconds: 300,
437 };
438
439 assert!(cached.is_valid());
440 }
441
442 #[test]
443 fn test_cached_score_expired() {
444 let score = ReputationScore {
445 user_id: Uuid::new_v4(),
446 overall_score: dec!(500),
447 tier: ReputationTier::Silver,
448 components: Default::default(),
449 };
450
451 let cached = CachedScore {
452 score,
453 cached_at: Utc::now() - chrono::Duration::seconds(400),
454 ttl_seconds: 300,
455 };
456
457 assert!(!cached.is_valid());
458 }
459
460 #[test]
461 fn test_cache_set_and_get_score() {
462 let cache = ReputationCache::new(300);
463 let user_id = Uuid::new_v4();
464 let score = ReputationScore {
465 user_id,
466 overall_score: dec!(500),
467 tier: ReputationTier::Silver,
468 components: Default::default(),
469 };
470
471 cache.set_score(user_id, score.clone()).unwrap();
472 let cached_score = cache.get_score(user_id).unwrap();
473
474 assert_eq!(cached_score.overall_score, score.overall_score);
475 assert_eq!(cached_score.tier, score.tier);
476 }
477
478 #[test]
479 fn test_cache_invalidate_score() {
480 let cache = ReputationCache::new(300);
481 let user_id = Uuid::new_v4();
482 let score = ReputationScore {
483 user_id,
484 overall_score: dec!(500),
485 tier: ReputationTier::Silver,
486 components: Default::default(),
487 };
488
489 cache.set_score(user_id, score).unwrap();
490 assert!(cache.get_score(user_id).is_some());
491
492 cache.invalidate_score(user_id);
493 assert!(cache.get_score(user_id).is_none());
494 }
495
496 #[test]
497 fn test_cache_tier() {
498 let cache = ReputationCache::new(300);
499 let user_id = Uuid::new_v4();
500 let tier = ReputationTier::Gold;
501 let score = dec!(750);
502
503 cache.set_tier(user_id, tier, score).unwrap();
504 let cached = cache.get_tier(user_id).unwrap();
505
506 assert_eq!(cached.0, tier);
507 assert_eq!(cached.1, score);
508 }
509
510 #[test]
511 fn test_cache_stats() {
512 let cache = ReputationCache::new(300);
513 let user_id = Uuid::new_v4();
514 let score = ReputationScore {
515 user_id,
516 overall_score: dec!(500),
517 tier: ReputationTier::Silver,
518 components: Default::default(),
519 };
520
521 cache.set_score(user_id, score).unwrap();
522 cache
523 .set_tier(user_id, ReputationTier::Silver, dec!(500))
524 .unwrap();
525
526 let stats = cache.get_stats();
527 assert_eq!(stats.cached_scores, 1);
528 assert_eq!(stats.cached_tiers, 1);
529 assert_eq!(stats.ttl_seconds, 300);
530 }
531
532 #[test]
533 fn test_cache_hit_rate_tracking() {
534 let cache = ReputationCache::new(300);
535 let user_id = Uuid::new_v4();
536 let score = ReputationScore {
537 user_id,
538 overall_score: dec!(500),
539 tier: ReputationTier::Silver,
540 components: Default::default(),
541 };
542
543 cache.set_score(user_id, score).unwrap();
545 cache
546 .set_tier(user_id, ReputationTier::Silver, dec!(500))
547 .unwrap();
548
549 assert!(cache.get_score(user_id).is_some());
551 assert!(cache.get_tier(user_id).is_some());
552
553 let other_user = Uuid::new_v4();
555 assert!(cache.get_score(other_user).is_none());
556 assert!(cache.get_tier(other_user).is_none());
557
558 let stats = cache.get_stats();
559 assert_eq!(stats.score_hits, 1);
560 assert_eq!(stats.score_misses, 1);
561 assert_eq!(stats.tier_hits, 1);
562 assert_eq!(stats.tier_misses, 1);
563 assert_eq!(stats.score_hit_rate, 0.5);
564 assert_eq!(stats.tier_hit_rate, 0.5);
565 assert_eq!(stats.overall_hit_rate, 0.5);
566 }
567
568 #[test]
569 fn test_cache_hit_rate_all_hits() {
570 let cache = ReputationCache::new(300);
571 let user_id = Uuid::new_v4();
572 let score = ReputationScore {
573 user_id,
574 overall_score: dec!(500),
575 tier: ReputationTier::Silver,
576 components: Default::default(),
577 };
578
579 cache.set_score(user_id, score).unwrap();
580 cache
581 .set_tier(user_id, ReputationTier::Silver, dec!(500))
582 .unwrap();
583
584 for _ in 0..5 {
586 assert!(cache.get_score(user_id).is_some());
587 assert!(cache.get_tier(user_id).is_some());
588 }
589
590 let stats = cache.get_stats();
591 assert_eq!(stats.score_hits, 5);
592 assert_eq!(stats.score_misses, 0);
593 assert_eq!(stats.tier_hits, 5);
594 assert_eq!(stats.tier_misses, 0);
595 assert_eq!(stats.score_hit_rate, 1.0);
596 assert_eq!(stats.tier_hit_rate, 1.0);
597 assert_eq!(stats.overall_hit_rate, 1.0);
598 assert!(stats.is_healthy());
599 }
600
601 #[test]
602 fn test_cache_hit_rate_all_misses() {
603 let cache = ReputationCache::new(300);
604
605 for _ in 0..5 {
607 let user_id = Uuid::new_v4();
608 assert!(cache.get_score(user_id).is_none());
609 assert!(cache.get_tier(user_id).is_none());
610 }
611
612 let stats = cache.get_stats();
613 assert_eq!(stats.score_hits, 0);
614 assert_eq!(stats.score_misses, 5);
615 assert_eq!(stats.tier_hits, 0);
616 assert_eq!(stats.tier_misses, 5);
617 assert_eq!(stats.score_hit_rate, 0.0);
618 assert_eq!(stats.tier_hit_rate, 0.0);
619 assert_eq!(stats.overall_hit_rate, 0.0);
620 assert!(stats.is_poor());
621 }
622
623 #[test]
624 fn test_cache_reset_metrics() {
625 let cache = ReputationCache::new(300);
626 let user_id = Uuid::new_v4();
627 let score = ReputationScore {
628 user_id,
629 overall_score: dec!(500),
630 tier: ReputationTier::Silver,
631 components: Default::default(),
632 };
633
634 cache.set_score(user_id, score).unwrap();
635 cache.get_score(user_id);
636 cache.get_score(Uuid::new_v4());
637
638 let stats_before = cache.get_stats();
639 assert_eq!(stats_before.score_hits, 1);
640 assert_eq!(stats_before.score_misses, 1);
641
642 cache.reset_metrics();
643
644 let stats_after = cache.get_stats();
645 assert_eq!(stats_after.score_hits, 0);
646 assert_eq!(stats_after.score_misses, 0);
647 assert_eq!(stats_after.tier_hits, 0);
648 assert_eq!(stats_after.tier_misses, 0);
649 }
650
651 #[test]
652 fn test_cache_stats_helpers() {
653 let cache = ReputationCache::new(300);
654 let user_id = Uuid::new_v4();
655 let score = ReputationScore {
656 user_id,
657 overall_score: dec!(500),
658 tier: ReputationTier::Silver,
659 components: Default::default(),
660 };
661
662 cache.set_score(user_id, score).unwrap();
663
664 for _ in 0..8 {
666 cache.get_score(user_id);
667 }
668 for _ in 0..2 {
669 cache.get_score(Uuid::new_v4());
670 }
671
672 let stats = cache.get_stats();
673 assert_eq!(stats.total_operations(), 10);
674 assert!(stats.is_healthy()); assert!(!stats.is_poor());
676 }
677
678 #[test]
679 fn test_cache_clear_all() {
680 let cache = ReputationCache::new(300);
681 let user_id = Uuid::new_v4();
682 let score = ReputationScore {
683 user_id,
684 overall_score: dec!(500),
685 tier: ReputationTier::Silver,
686 components: Default::default(),
687 };
688
689 cache.set_score(user_id, score).unwrap();
690 cache
691 .set_tier(user_id, ReputationTier::Silver, dec!(500))
692 .unwrap();
693
694 cache.clear_all();
695
696 let stats = cache.get_stats();
697 assert_eq!(stats.cached_scores, 0);
698 assert_eq!(stats.cached_tiers, 0);
699 }
700
701 #[test]
702 fn test_remaining_ttl() {
703 let score = ReputationScore {
704 user_id: Uuid::new_v4(),
705 overall_score: dec!(500),
706 tier: ReputationTier::Silver,
707 components: Default::default(),
708 };
709
710 let cached = CachedScore {
711 score,
712 cached_at: Utc::now(),
713 ttl_seconds: 300,
714 };
715
716 let remaining = cached.remaining_ttl();
717 assert!(remaining > 0 && remaining <= 300);
718 }
719
720 mod proptest_cache {
725 use super::*;
726 use proptest::prelude::*;
727
728 fn ttl_strategy() -> impl Strategy<Value = i64> {
730 1i64..=3600i64
731 }
732
733 fn score_strategy() -> impl Strategy<Value = i64> {
735 0i64..=1000i64
736 }
737
738 proptest! {
739 #[test]
741 fn prop_cache_score_consistency(
742 score_val in score_strategy(),
743 ttl in ttl_strategy(),
744 ) {
745 let cache = ReputationCache::new(ttl);
746 let user_id = Uuid::new_v4();
747 let score = ReputationScore {
748 user_id,
749 overall_score: Decimal::new(score_val, 0),
750 tier: ReputationTier::from_score(Decimal::new(score_val, 0)),
751 components: Default::default(),
752 };
753
754 cache.set_score(user_id, score.clone()).unwrap();
755 let retrieved = cache.get_score(user_id);
756
757 prop_assert!(retrieved.is_some());
758 if let Some(cached_score) = retrieved {
759 prop_assert_eq!(cached_score.overall_score, score.overall_score);
760 prop_assert_eq!(cached_score.user_id, user_id);
761 }
762 }
763
764 #[test]
766 fn prop_cache_tier_consistency(
767 score_val in score_strategy(),
768 ttl in ttl_strategy(),
769 ) {
770 let cache = ReputationCache::new(ttl);
771 let user_id = Uuid::new_v4();
772 let tier = ReputationTier::from_score(Decimal::new(score_val, 0));
773 let score_dec = Decimal::new(score_val, 0);
774
775 cache.set_tier(user_id, tier, score_dec).unwrap();
776 let retrieved = cache.get_tier(user_id);
777
778 prop_assert!(retrieved.is_some());
779 if let Some((cached_tier, cached_score)) = retrieved {
780 prop_assert_eq!(cached_tier, tier);
781 prop_assert_eq!(cached_score, score_dec);
782 }
783 }
784
785 #[test]
787 fn prop_cache_invalidation(
788 score_val in score_strategy(),
789 ttl in ttl_strategy(),
790 ) {
791 let cache = ReputationCache::new(ttl);
792 let user_id = Uuid::new_v4();
793 let score = ReputationScore {
794 user_id,
795 overall_score: Decimal::new(score_val, 0),
796 tier: ReputationTier::from_score(Decimal::new(score_val, 0)),
797 components: Default::default(),
798 };
799
800 cache.set_score(user_id, score.clone()).unwrap();
801 cache.set_tier(user_id, score.tier, score.overall_score).unwrap();
802
803 cache.invalidate_user(user_id);
804
805 prop_assert!(cache.get_score(user_id).is_none());
806 prop_assert!(cache.get_tier(user_id).is_none());
807 }
808
809 #[test]
811 fn prop_cache_stats_accuracy(
812 num_users in 1usize..=10usize,
813 score_val in score_strategy(),
814 ttl in ttl_strategy(),
815 ) {
816 let cache = ReputationCache::new(ttl);
817 let user_ids: Vec<Uuid> = (0..num_users).map(|_| Uuid::new_v4()).collect();
818
819 for user_id in &user_ids {
820 let score = ReputationScore {
821 user_id: *user_id,
822 overall_score: Decimal::new(score_val, 0),
823 tier: ReputationTier::from_score(Decimal::new(score_val, 0)),
824 components: Default::default(),
825 };
826 cache.set_score(*user_id, score).unwrap();
827 }
828
829 let stats = cache.get_stats();
830 prop_assert_eq!(stats.cached_scores, num_users);
831 }
832
833 #[test]
835 fn prop_cache_clear_all(
836 num_users in 1usize..=10usize,
837 score_val in score_strategy(),
838 ttl in ttl_strategy(),
839 ) {
840 let cache = ReputationCache::new(ttl);
841 let user_ids: Vec<Uuid> = (0..num_users).map(|_| Uuid::new_v4()).collect();
842
843 for user_id in &user_ids {
844 let score = ReputationScore {
845 user_id: *user_id,
846 overall_score: Decimal::new(score_val, 0),
847 tier: ReputationTier::from_score(Decimal::new(score_val, 0)),
848 components: Default::default(),
849 };
850 cache.set_score(*user_id, score).unwrap();
851 cache.set_tier(*user_id, ReputationTier::from_score(Decimal::new(score_val, 0)), Decimal::new(score_val, 0)).unwrap();
852 }
853
854 cache.clear_all();
855
856 let stats = cache.get_stats();
857 prop_assert_eq!(stats.cached_scores, 0);
858 prop_assert_eq!(stats.cached_tiers, 0);
859 }
860
861 #[test]
863 fn prop_remaining_ttl_bounds(
864 score_val in score_strategy(),
865 ttl in ttl_strategy(),
866 ) {
867 let score = ReputationScore {
868 user_id: Uuid::new_v4(),
869 overall_score: Decimal::new(score_val, 0),
870 tier: ReputationTier::from_score(Decimal::new(score_val, 0)),
871 components: Default::default(),
872 };
873
874 let cached = CachedScore {
875 score,
876 cached_at: Utc::now(),
877 ttl_seconds: ttl,
878 };
879
880 let remaining = cached.remaining_ttl();
881 prop_assert!(remaining >= 0);
882 prop_assert!(remaining <= ttl);
883 }
884 }
885 }
886}