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