1use serde::{Deserialize, Serialize};
41use std::cmp::Ordering;
42use std::collections::{BinaryHeap, HashMap};
43
44const DEFAULT_FREQUENCY_WEIGHT: f64 = 0.3;
46
47const DEFAULT_SIZE_WEIGHT: f64 = 0.2;
49
50const DEFAULT_REVENUE_WEIGHT: f64 = 0.3;
52
53const DEFAULT_RECENCY_WEIGHT: f64 = 0.2;
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ContentPriority {
59 pub manual_priority: u8,
61 pub access_frequency: f64,
63 pub size_bytes: u64,
65 pub revenue_per_gb: f64,
67 pub last_access_age_secs: u64,
69}
70
71impl ContentPriority {
72 #[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 #[must_use]
86 pub const fn with_manual_priority(mut self, priority: u8) -> Self {
87 self.manual_priority = priority;
88 self
89 }
90
91 #[must_use]
93 pub const fn with_frequency(mut self, frequency: f64) -> Self {
94 self.access_frequency = frequency;
95 self
96 }
97
98 #[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#[derive(Debug, Clone)]
108pub struct EvictionConfig {
109 pub frequency_weight: f64,
111 pub size_weight: f64,
113 pub revenue_weight: f64,
115 pub recency_weight: f64,
117 pub manual_priority_multiplier: f64,
119}
120
121impl EvictionConfig {
122 #[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 #[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 #[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 #[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#[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 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 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 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 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 other
248 .score
249 .partial_cmp(&self.score)
250 .unwrap_or(Ordering::Equal)
251 }
252}
253
254#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256pub struct EvictionStats {
257 pub total_entries: usize,
259 pub total_bytes: u64,
261 pub evictions_performed: u64,
263 pub bytes_evicted: u64,
265 pub avg_evicted_score: f64,
267 pub avg_retained_score: f64,
269}
270
271pub struct PriorityEvictor {
273 config: EvictionConfig,
275 entries: HashMap<String, ContentPriority>,
277 stats: EvictionStats,
279}
280
281impl PriorityEvictor {
282 #[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 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 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 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 #[must_use]
326 pub fn get_eviction_candidates(&self, bytes_to_free: u64) -> Vec<String> {
327 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 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 #[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 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 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 #[must_use]
399 #[inline]
400 pub fn stats(&self) -> &EvictionStats {
401 &self.stats
402 }
403
404 #[must_use]
406 #[inline]
407 pub fn entry_count(&self) -> usize {
408 self.entries.len()
409 }
410
411 #[must_use]
413 #[inline]
414 pub fn total_bytes(&self) -> u64 {
415 self.stats.total_bytes
416 }
417
418 #[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 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 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 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 let candidates = evictor.get_eviction_candidates(3000);
536 assert!(candidates.len() >= 2); }
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 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); }
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), );
624 evictor.add_content(
625 "small".to_string(),
626 ContentPriority::new(1024), );
628
629 let candidates = evictor.get_lowest_priority(1);
631 assert_eq!(candidates[0], "large");
632 }
633}