chie_core/
priority_eviction.rs

1//! Priority-based content eviction.
2//!
3//! This module provides intelligent content eviction based on priority scores,
4//! combining multiple factors such as access frequency, size, revenue potential,
5//! and manual priority levels to make optimal eviction decisions.
6//!
7//! # Features
8//!
9//! - Multi-factor priority scoring
10//! - Weighted priority components
11//! - Adaptive eviction based on resource pressure
12//! - Revenue-aware eviction for monetization
13//! - Custom priority calculators
14//! - Eviction history tracking
15//!
16//! # Example
17//!
18//! ```
19//! use chie_core::priority_eviction::{PriorityEvictor, EvictionConfig, ContentPriority};
20//!
21//! # fn example() {
22//! let config = EvictionConfig::default();
23//! let mut evictor = PriorityEvictor::new(config);
24//!
25//! // Add content with priorities
26//! evictor.add_content("content:1".to_string(), ContentPriority {
27//!     manual_priority: 5,
28//!     access_frequency: 10.0,
29//!     size_bytes: 1024,
30//!     revenue_per_gb: 5.0,
31//!     last_access_age_secs: 3600,
32//! });
33//!
34//! // Get candidates for eviction
35//! let candidates = evictor.get_eviction_candidates(1024 * 1024);
36//! println!("Eviction candidates: {:?}", candidates);
37//! # }
38//! ```
39
40use serde::{Deserialize, Serialize};
41use std::cmp::Ordering;
42use std::collections::{BinaryHeap, HashMap};
43
44/// Default weight for access frequency in priority calculation
45const DEFAULT_FREQUENCY_WEIGHT: f64 = 0.3;
46
47/// Default weight for size in priority calculation (larger = lower priority)
48const DEFAULT_SIZE_WEIGHT: f64 = 0.2;
49
50/// Default weight for revenue potential in priority calculation
51const DEFAULT_REVENUE_WEIGHT: f64 = 0.3;
52
53/// Default weight for recency in priority calculation
54const DEFAULT_RECENCY_WEIGHT: f64 = 0.2;
55
56/// Content priority factors
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ContentPriority {
59    /// Manual priority level (0-10, higher = more important)
60    pub manual_priority: u8,
61    /// Access frequency (accesses per hour)
62    pub access_frequency: f64,
63    /// Size in bytes
64    pub size_bytes: u64,
65    /// Revenue per GB (for monetized content)
66    pub revenue_per_gb: f64,
67    /// Age since last access in seconds
68    pub last_access_age_secs: u64,
69}
70
71impl ContentPriority {
72    /// Create a new content priority with default values
73    #[must_use]
74    pub const fn new(size_bytes: u64) -> Self {
75        Self {
76            manual_priority: 5,
77            access_frequency: 0.0,
78            size_bytes,
79            revenue_per_gb: 0.0,
80            last_access_age_secs: 0,
81        }
82    }
83
84    /// Set manual priority
85    #[must_use]
86    pub const fn with_manual_priority(mut self, priority: u8) -> Self {
87        self.manual_priority = priority;
88        self
89    }
90
91    /// Set access frequency
92    #[must_use]
93    pub const fn with_frequency(mut self, frequency: f64) -> Self {
94        self.access_frequency = frequency;
95        self
96    }
97
98    /// Set revenue per GB
99    #[must_use]
100    pub const fn with_revenue(mut self, revenue_per_gb: f64) -> Self {
101        self.revenue_per_gb = revenue_per_gb;
102        self
103    }
104}
105
106/// Configuration for priority-based eviction
107#[derive(Debug, Clone)]
108pub struct EvictionConfig {
109    /// Weight for access frequency (0.0-1.0)
110    pub frequency_weight: f64,
111    /// Weight for size penalty (0.0-1.0)
112    pub size_weight: f64,
113    /// Weight for revenue potential (0.0-1.0)
114    pub revenue_weight: f64,
115    /// Weight for recency (0.0-1.0)
116    pub recency_weight: f64,
117    /// Manual priority multiplier
118    pub manual_priority_multiplier: f64,
119}
120
121impl EvictionConfig {
122    /// Create a new configuration with custom weights
123    #[must_use]
124    pub const fn new(
125        frequency_weight: f64,
126        size_weight: f64,
127        revenue_weight: f64,
128        recency_weight: f64,
129        manual_priority_multiplier: f64,
130    ) -> Self {
131        Self {
132            frequency_weight,
133            size_weight,
134            revenue_weight,
135            recency_weight,
136            manual_priority_multiplier,
137        }
138    }
139
140    /// Create a revenue-focused configuration (prioritize high-revenue content)
141    #[must_use]
142    pub const fn revenue_focused() -> Self {
143        Self {
144            frequency_weight: 0.2,
145            size_weight: 0.1,
146            revenue_weight: 0.6,
147            recency_weight: 0.1,
148            manual_priority_multiplier: 2.0,
149        }
150    }
151
152    /// Create a performance-focused configuration (prioritize frequently accessed)
153    #[must_use]
154    pub const fn performance_focused() -> Self {
155        Self {
156            frequency_weight: 0.5,
157            size_weight: 0.2,
158            revenue_weight: 0.1,
159            recency_weight: 0.2,
160            manual_priority_multiplier: 1.5,
161        }
162    }
163
164    /// Create a space-focused configuration (prioritize small files)
165    #[must_use]
166    pub const fn space_focused() -> Self {
167        Self {
168            frequency_weight: 0.2,
169            size_weight: 0.5,
170            revenue_weight: 0.1,
171            recency_weight: 0.2,
172            manual_priority_multiplier: 1.0,
173        }
174    }
175}
176
177impl Default for EvictionConfig {
178    fn default() -> Self {
179        Self {
180            frequency_weight: DEFAULT_FREQUENCY_WEIGHT,
181            size_weight: DEFAULT_SIZE_WEIGHT,
182            revenue_weight: DEFAULT_REVENUE_WEIGHT,
183            recency_weight: DEFAULT_RECENCY_WEIGHT,
184            manual_priority_multiplier: 2.0,
185        }
186    }
187}
188
189/// Content entry with calculated priority score
190#[derive(Debug, Clone)]
191struct PriorityEntry {
192    cid: String,
193    priority: ContentPriority,
194    score: f64,
195}
196
197impl PriorityEntry {
198    fn new(cid: String, priority: ContentPriority, config: &EvictionConfig) -> Self {
199        let score = Self::calculate_score(&priority, config);
200        Self {
201            cid,
202            priority,
203            score,
204        }
205    }
206
207    fn calculate_score(priority: &ContentPriority, config: &EvictionConfig) -> f64 {
208        // Normalize factors to 0.0-1.0 range
209        let manual_factor =
210            (priority.manual_priority as f64 / 10.0) * config.manual_priority_multiplier;
211
212        let frequency_factor =
213            (priority.access_frequency.min(100.0) / 100.0) * config.frequency_weight;
214
215        // Size penalty (larger = lower priority)
216        let size_mb = priority.size_bytes as f64 / (1024.0 * 1024.0);
217        let size_factor = (1.0 / (1.0 + size_mb)) * config.size_weight;
218
219        let revenue_factor = (priority.revenue_per_gb.min(100.0) / 100.0) * config.revenue_weight;
220
221        // Recency (newer access = higher priority)
222        let age_hours = priority.last_access_age_secs as f64 / 3600.0;
223        let recency_factor = (1.0 / (1.0 + age_hours)) * config.recency_weight;
224
225        // Combine all factors
226        manual_factor + frequency_factor + size_factor + revenue_factor + recency_factor
227    }
228}
229
230impl PartialEq for PriorityEntry {
231    fn eq(&self, other: &Self) -> bool {
232        self.score == other.score
233    }
234}
235
236impl Eq for PriorityEntry {}
237
238impl PartialOrd for PriorityEntry {
239    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
240        Some(self.cmp(other))
241    }
242}
243
244impl Ord for PriorityEntry {
245    fn cmp(&self, other: &Self) -> Ordering {
246        // Reverse ordering for min-heap (lowest priority first)
247        other
248            .score
249            .partial_cmp(&self.score)
250            .unwrap_or(Ordering::Equal)
251    }
252}
253
254/// Statistics for eviction operations
255#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256pub struct EvictionStats {
257    /// Total content entries tracked
258    pub total_entries: usize,
259    /// Total bytes tracked
260    pub total_bytes: u64,
261    /// Number of evictions performed
262    pub evictions_performed: u64,
263    /// Total bytes evicted
264    pub bytes_evicted: u64,
265    /// Average priority score of evicted content
266    pub avg_evicted_score: f64,
267    /// Average priority score of retained content
268    pub avg_retained_score: f64,
269}
270
271/// Priority-based content evictor
272pub struct PriorityEvictor {
273    /// Configuration
274    config: EvictionConfig,
275    /// Content entries (cid -> priority)
276    entries: HashMap<String, ContentPriority>,
277    /// Statistics
278    stats: EvictionStats,
279}
280
281impl PriorityEvictor {
282    /// Create a new priority evictor
283    #[must_use]
284    pub fn new(config: EvictionConfig) -> Self {
285        Self {
286            config,
287            entries: HashMap::new(),
288            stats: EvictionStats::default(),
289        }
290    }
291
292    /// Add content to track for eviction
293    pub fn add_content(&mut self, cid: String, priority: ContentPriority) {
294        let size = priority.size_bytes;
295        self.entries.insert(cid, priority);
296        self.stats.total_entries = self.entries.len();
297        self.stats.total_bytes += size;
298    }
299
300    /// Update priority for existing content
301    pub fn update_priority(&mut self, cid: &str, priority: ContentPriority) -> bool {
302        if let Some(old_priority) = self.entries.get_mut(cid) {
303            let old_size = old_priority.size_bytes;
304            let new_size = priority.size_bytes;
305            *old_priority = priority;
306            self.stats.total_bytes = self.stats.total_bytes.saturating_sub(old_size) + new_size;
307            true
308        } else {
309            false
310        }
311    }
312
313    /// Remove content from tracking
314    pub fn remove_content(&mut self, cid: &str) -> Option<ContentPriority> {
315        if let Some(priority) = self.entries.remove(cid) {
316            self.stats.total_entries = self.entries.len();
317            self.stats.total_bytes = self.stats.total_bytes.saturating_sub(priority.size_bytes);
318            Some(priority)
319        } else {
320            None
321        }
322    }
323
324    /// Get eviction candidates to free up specified bytes
325    #[must_use]
326    pub fn get_eviction_candidates(&self, bytes_to_free: u64) -> Vec<String> {
327        // Build min-heap of all content (lowest priority first)
328        let mut heap: BinaryHeap<PriorityEntry> = self
329            .entries
330            .iter()
331            .map(|(cid, priority)| PriorityEntry::new(cid.clone(), priority.clone(), &self.config))
332            .collect();
333
334        let mut candidates = Vec::new();
335        let mut bytes_freed = 0u64;
336
337        // Pop lowest priority items until we've freed enough space
338        while let Some(entry) = heap.pop() {
339            bytes_freed += entry.priority.size_bytes;
340            candidates.push(entry.cid);
341
342            if bytes_freed >= bytes_to_free {
343                break;
344            }
345        }
346
347        candidates
348    }
349
350    /// Get N lowest priority items for eviction
351    #[must_use]
352    pub fn get_lowest_priority(&self, count: usize) -> Vec<String> {
353        let mut heap: BinaryHeap<PriorityEntry> = self
354            .entries
355            .iter()
356            .map(|(cid, priority)| PriorityEntry::new(cid.clone(), priority.clone(), &self.config))
357            .collect();
358
359        let mut result = Vec::new();
360        for _ in 0..count.min(heap.len()) {
361            if let Some(entry) = heap.pop() {
362                result.push(entry.cid);
363            }
364        }
365
366        result
367    }
368
369    /// Evict content and update statistics
370    pub fn evict(&mut self, candidates: &[String]) {
371        let mut total_score = 0.0;
372
373        for cid in candidates {
374            if let Some(priority) = self.remove_content(cid) {
375                let score = PriorityEntry::calculate_score(&priority, &self.config);
376                total_score += score;
377                self.stats.evictions_performed += 1;
378                self.stats.bytes_evicted += priority.size_bytes;
379            }
380        }
381
382        if !candidates.is_empty() {
383            self.stats.avg_evicted_score = total_score / candidates.len() as f64;
384        }
385
386        // Update retained average
387        if !self.entries.is_empty() {
388            let retained_score: f64 = self
389                .entries
390                .values()
391                .map(|p| PriorityEntry::calculate_score(p, &self.config))
392                .sum();
393            self.stats.avg_retained_score = retained_score / self.entries.len() as f64;
394        }
395    }
396
397    /// Get current statistics
398    #[must_use]
399    #[inline]
400    pub fn stats(&self) -> &EvictionStats {
401        &self.stats
402    }
403
404    /// Get number of tracked entries
405    #[must_use]
406    #[inline]
407    pub fn entry_count(&self) -> usize {
408        self.entries.len()
409    }
410
411    /// Get total bytes tracked
412    #[must_use]
413    #[inline]
414    pub fn total_bytes(&self) -> u64 {
415        self.stats.total_bytes
416    }
417
418    /// Get priority score for a specific content
419    #[must_use]
420    #[inline]
421    pub fn get_priority_score(&self, cid: &str) -> Option<f64> {
422        self.entries
423            .get(cid)
424            .map(|p| PriorityEntry::calculate_score(p, &self.config))
425    }
426
427    /// Update configuration
428    pub fn set_config(&mut self, config: EvictionConfig) {
429        self.config = config;
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_content_priority_builder() {
439        let priority = ContentPriority::new(1024)
440            .with_manual_priority(8)
441            .with_frequency(10.0)
442            .with_revenue(5.0);
443
444        assert_eq!(priority.manual_priority, 8);
445        assert_eq!(priority.access_frequency, 10.0);
446        assert_eq!(priority.revenue_per_gb, 5.0);
447    }
448
449    #[test]
450    fn test_eviction_config_presets() {
451        let revenue = EvictionConfig::revenue_focused();
452        assert!(revenue.revenue_weight > revenue.frequency_weight);
453
454        let performance = EvictionConfig::performance_focused();
455        assert!(performance.frequency_weight > performance.revenue_weight);
456
457        let space = EvictionConfig::space_focused();
458        assert!(space.size_weight > space.revenue_weight);
459    }
460
461    #[test]
462    fn test_priority_evictor_add() {
463        let config = EvictionConfig::default();
464        let mut evictor = PriorityEvictor::new(config);
465
466        let priority = ContentPriority::new(1024);
467        evictor.add_content("test:1".to_string(), priority);
468
469        assert_eq!(evictor.entry_count(), 1);
470        assert_eq!(evictor.total_bytes(), 1024);
471    }
472
473    #[test]
474    fn test_priority_evictor_update() {
475        let config = EvictionConfig::default();
476        let mut evictor = PriorityEvictor::new(config);
477
478        let priority1 = ContentPriority::new(1024);
479        evictor.add_content("test:1".to_string(), priority1);
480
481        let priority2 = ContentPriority::new(2048).with_manual_priority(8);
482        assert!(evictor.update_priority("test:1", priority2));
483
484        assert_eq!(evictor.total_bytes(), 2048);
485    }
486
487    #[test]
488    fn test_priority_evictor_remove() {
489        let config = EvictionConfig::default();
490        let mut evictor = PriorityEvictor::new(config);
491
492        let priority = ContentPriority::new(1024);
493        evictor.add_content("test:1".to_string(), priority);
494
495        let removed = evictor.remove_content("test:1");
496        assert!(removed.is_some());
497        assert_eq!(evictor.entry_count(), 0);
498        assert_eq!(evictor.total_bytes(), 0);
499    }
500
501    #[test]
502    fn test_eviction_candidates_by_bytes() {
503        let config = EvictionConfig::default();
504        let mut evictor = PriorityEvictor::new(config);
505
506        // Add content with different priorities
507        evictor.add_content(
508            "low_priority".to_string(),
509            ContentPriority::new(1024).with_manual_priority(1),
510        );
511        evictor.add_content(
512            "high_priority".to_string(),
513            ContentPriority::new(1024).with_manual_priority(9),
514        );
515
516        // Should evict low priority content first
517        let candidates = evictor.get_eviction_candidates(1024);
518        assert_eq!(candidates.len(), 1);
519        assert_eq!(candidates[0], "low_priority");
520    }
521
522    #[test]
523    fn test_eviction_candidates_multiple() {
524        let config = EvictionConfig::default();
525        let mut evictor = PriorityEvictor::new(config);
526
527        for i in 0..5 {
528            evictor.add_content(
529                format!("content:{i}"),
530                ContentPriority::new(1024).with_manual_priority(i),
531            );
532        }
533
534        // Need to evict multiple items
535        let candidates = evictor.get_eviction_candidates(3000);
536        assert!(candidates.len() >= 2); // Should evict at least 2 items (2048 bytes)
537    }
538
539    #[test]
540    fn test_get_lowest_priority() {
541        let config = EvictionConfig::default();
542        let mut evictor = PriorityEvictor::new(config);
543
544        for i in 0..10u8 {
545            evictor.add_content(
546                format!("content:{i}"),
547                ContentPriority::new(1024).with_manual_priority(i),
548            );
549        }
550
551        let lowest = evictor.get_lowest_priority(3);
552        assert_eq!(lowest.len(), 3);
553        // Lowest priorities should be content:0, content:1, content:2
554        assert!(lowest.contains(&"content:0".to_string()));
555    }
556
557    #[test]
558    fn test_evict_and_stats() {
559        let config = EvictionConfig::default();
560        let mut evictor = PriorityEvictor::new(config);
561
562        evictor.add_content(
563            "test:1".to_string(),
564            ContentPriority::new(1024).with_manual_priority(1),
565        );
566        evictor.add_content(
567            "test:2".to_string(),
568            ContentPriority::new(2048).with_manual_priority(5),
569        );
570
571        let candidates = vec!["test:1".to_string()];
572        evictor.evict(&candidates);
573
574        let stats = evictor.stats();
575        assert_eq!(stats.evictions_performed, 1);
576        assert_eq!(stats.bytes_evicted, 1024);
577        assert_eq!(evictor.entry_count(), 1);
578    }
579
580    #[test]
581    fn test_priority_score_calculation() {
582        let config = EvictionConfig::default();
583        let mut evictor = PriorityEvictor::new(config);
584
585        let priority = ContentPriority::new(1024)
586            .with_manual_priority(8)
587            .with_frequency(50.0)
588            .with_revenue(10.0);
589
590        evictor.add_content("test:1".to_string(), priority);
591
592        let score = evictor.get_priority_score("test:1").unwrap();
593        assert!(score > 0.0);
594        assert!(score < 10.0); // Should be reasonable
595    }
596
597    #[test]
598    fn test_revenue_focused_priority() {
599        let config = EvictionConfig::revenue_focused();
600        let mut evictor = PriorityEvictor::new(config);
601
602        evictor.add_content(
603            "high_revenue".to_string(),
604            ContentPriority::new(1024).with_revenue(50.0),
605        );
606        evictor.add_content(
607            "low_revenue".to_string(),
608            ContentPriority::new(1024).with_revenue(1.0),
609        );
610
611        let candidates = evictor.get_lowest_priority(1);
612        assert_eq!(candidates[0], "low_revenue");
613    }
614
615    #[test]
616    fn test_size_penalty() {
617        let config = EvictionConfig::space_focused();
618        let mut evictor = PriorityEvictor::new(config);
619
620        evictor.add_content(
621            "large".to_string(),
622            ContentPriority::new(10 * 1024 * 1024), // 10 MB
623        );
624        evictor.add_content(
625            "small".to_string(),
626            ContentPriority::new(1024), // 1 KB
627        );
628
629        // Large files should have lower priority with space-focused config
630        let candidates = evictor.get_lowest_priority(1);
631        assert_eq!(candidates[0], "large");
632    }
633}