1use serde::{Deserialize, Serialize};
37use std::collections::{HashMap, VecDeque};
38use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
39
40const DEFAULT_MAX_ENTRIES: usize = 100_000;
42
43#[allow(dead_code)]
45const DEFAULT_BATCH_SIZE: usize = 1000;
46
47#[derive(Debug, Clone)]
49pub struct ContentEntry {
50 pub cid: String,
52 pub size_bytes: u64,
54 pub created_at: Instant,
56 pub last_accessed: Instant,
58 pub access_count: u64,
60 pub expires_at: Option<Instant>,
62}
63
64impl ContentEntry {
65 #[must_use]
67 pub fn new(cid: String, size_bytes: u64) -> Self {
68 let now = Instant::now();
69 Self {
70 cid,
71 size_bytes,
72 created_at: now,
73 last_accessed: now,
74 access_count: 0,
75 expires_at: None,
76 }
77 }
78
79 #[must_use]
81 pub fn with_expiration(cid: String, size_bytes: u64, expires_at: Instant) -> Self {
82 let now = Instant::now();
83 Self {
84 cid,
85 size_bytes,
86 created_at: now,
87 last_accessed: now,
88 access_count: 0,
89 expires_at: Some(expires_at),
90 }
91 }
92
93 pub fn record_access(&mut self) {
95 self.last_accessed = Instant::now();
96 self.access_count += 1;
97 }
98
99 #[must_use]
101 #[inline]
102 pub fn age(&self) -> Duration {
103 self.created_at.elapsed()
104 }
105
106 #[must_use]
108 #[inline]
109 pub fn idle_time(&self) -> Duration {
110 self.last_accessed.elapsed()
111 }
112
113 #[must_use]
115 #[inline]
116 pub const fn has_explicit_expiration(&self) -> bool {
117 self.expires_at.is_some()
118 }
119}
120
121#[derive(Debug, Clone, Default)]
123pub enum ExpirationPolicy {
124 Ttl(Duration),
126
127 IdleTimeout(Duration),
129
130 Lru(usize),
132
133 SizeQuota(u64),
135
136 Combined(Vec<ExpirationPolicy>),
138
139 #[default]
141 Never,
142}
143
144impl ExpirationPolicy {
145 #[must_use]
147 pub const fn ttl(duration: Duration) -> Self {
148 Self::Ttl(duration)
149 }
150
151 #[must_use]
153 pub const fn idle_timeout(duration: Duration) -> Self {
154 Self::IdleTimeout(duration)
155 }
156
157 #[must_use]
159 pub const fn lru(max_entries: usize) -> Self {
160 Self::Lru(max_entries)
161 }
162
163 #[must_use]
165 pub const fn size_quota(max_bytes: u64) -> Self {
166 Self::SizeQuota(max_bytes)
167 }
168
169 #[must_use]
171 pub fn combined(policies: Vec<Self>) -> Self {
172 Self::Combined(policies)
173 }
174
175 #[must_use]
177 #[inline]
178 pub fn should_expire(&self, entry: &ContentEntry) -> bool {
179 if let Some(expires_at) = entry.expires_at {
181 if Instant::now() >= expires_at {
182 return true;
183 }
184 }
185
186 match self {
187 Self::Ttl(duration) => entry.age() >= *duration,
188 Self::IdleTimeout(duration) => entry.idle_time() >= *duration,
189 Self::Combined(policies) => policies.iter().any(|p| p.should_expire(entry)),
190 Self::Never | Self::Lru(_) | Self::SizeQuota(_) => false,
191 }
192 }
193}
194
195#[derive(Debug, Clone, Default, Serialize, Deserialize)]
197pub struct ExpirationStats {
198 pub total_entries: usize,
200 pub total_bytes: u64,
202 pub expired_count: u64,
204 pub bytes_freed: u64,
206 pub checks_performed: u64,
208 pub last_check_ms: u64,
210}
211
212pub struct ExpirationManager {
214 policy: ExpirationPolicy,
216 entries: HashMap<String, ContentEntry>,
218 access_order: VecDeque<String>,
220 stats: ExpirationStats,
222 max_entries: usize,
224}
225
226impl ExpirationManager {
227 #[must_use]
229 pub fn new(policy: ExpirationPolicy) -> Self {
230 Self {
231 policy,
232 entries: HashMap::new(),
233 access_order: VecDeque::new(),
234 stats: ExpirationStats::default(),
235 max_entries: DEFAULT_MAX_ENTRIES,
236 }
237 }
238
239 #[must_use]
241 pub fn with_max_entries(policy: ExpirationPolicy, max_entries: usize) -> Self {
242 Self {
243 policy,
244 entries: HashMap::new(),
245 access_order: VecDeque::new(),
246 stats: ExpirationStats::default(),
247 max_entries,
248 }
249 }
250
251 pub fn register(&mut self, cid: String, size_bytes: u64) {
253 let entry = ContentEntry::new(cid.clone(), size_bytes);
254 self.insert_entry(entry);
255 }
256
257 pub fn register_with_expiration(&mut self, cid: String, size_bytes: u64, expires_at: Instant) {
259 let entry = ContentEntry::with_expiration(cid.clone(), size_bytes, expires_at);
260 self.insert_entry(entry);
261 }
262
263 fn insert_entry(&mut self, entry: ContentEntry) {
265 let cid = entry.cid.clone();
266 let size = entry.size_bytes;
267
268 self.entries.insert(cid.clone(), entry);
269 self.access_order.push_back(cid);
270
271 self.stats.total_entries = self.entries.len();
272 self.stats.total_bytes += size;
273
274 if self.entries.len() > self.max_entries {
276 self.expire_oldest();
277 }
278 }
279
280 pub fn record_access(&mut self, cid: &str) {
282 if let Some(entry) = self.entries.get_mut(cid) {
283 entry.record_access();
284
285 if let Some(pos) = self.access_order.iter().position(|c| c == cid) {
287 self.access_order.remove(pos);
288 self.access_order.push_back(cid.to_string());
289 }
290 }
291 }
292
293 #[must_use]
295 pub fn get_expired(&mut self) -> Vec<String> {
296 self.stats.checks_performed += 1;
297 self.stats.last_check_ms = current_timestamp_ms();
298
299 let mut expired = Vec::new();
300
301 for (cid, entry) in &self.entries {
303 if self.policy.should_expire(entry) {
304 expired.push(cid.clone());
305 }
306 }
307
308 if let ExpirationPolicy::Lru(max_entries) = self.policy {
310 if self.entries.len() > max_entries {
311 let to_remove = self.entries.len() - max_entries;
312 for cid in self.access_order.iter().take(to_remove) {
313 if !expired.contains(cid) {
314 expired.push(cid.clone());
315 }
316 }
317 }
318 }
319
320 if let ExpirationPolicy::SizeQuota(max_bytes) = self.policy {
322 if self.stats.total_bytes > max_bytes {
323 let mut bytes_to_free = self.stats.total_bytes - max_bytes;
324 for cid in &self.access_order {
325 if bytes_to_free == 0 {
326 break;
327 }
328 if let Some(entry) = self.entries.get(cid) {
329 if !expired.contains(cid) {
330 expired.push(cid.clone());
331 bytes_to_free = bytes_to_free.saturating_sub(entry.size_bytes);
332 }
333 }
334 }
335 }
336 }
337
338 expired
339 }
340
341 pub fn expire(&mut self) -> Vec<String> {
343 let expired = self.get_expired();
344 for cid in &expired {
345 self.remove(cid);
346 }
347 expired
348 }
349
350 pub fn expire_batch(&mut self, batch_size: usize) -> Vec<String> {
352 let expired = self.get_expired();
353 let to_remove: Vec<_> = expired.into_iter().take(batch_size).collect();
354
355 for cid in &to_remove {
356 self.remove(cid);
357 }
358
359 to_remove
360 }
361
362 pub fn remove(&mut self, cid: &str) -> Option<ContentEntry> {
364 if let Some(entry) = self.entries.remove(cid) {
365 self.stats.total_entries = self.entries.len();
367 self.stats.total_bytes = self.stats.total_bytes.saturating_sub(entry.size_bytes);
368 self.stats.expired_count += 1;
369 self.stats.bytes_freed += entry.size_bytes;
370
371 if let Some(pos) = self.access_order.iter().position(|c| c == cid) {
373 self.access_order.remove(pos);
374 }
375
376 Some(entry)
377 } else {
378 None
379 }
380 }
381
382 fn expire_oldest(&mut self) {
384 while self.entries.len() > self.max_entries {
385 if let Some(cid) = self.access_order.pop_front() {
386 self.remove(&cid);
387 } else {
388 break;
389 }
390 }
391 }
392
393 #[must_use]
395 #[inline]
396 pub fn stats(&self) -> &ExpirationStats {
397 &self.stats
398 }
399
400 #[must_use]
402 #[inline]
403 pub fn entry_count(&self) -> usize {
404 self.entries.len()
405 }
406
407 #[must_use]
409 #[inline]
410 pub fn total_bytes(&self) -> u64 {
411 self.stats.total_bytes
412 }
413
414 #[must_use]
416 #[inline]
417 pub fn contains(&self, cid: &str) -> bool {
418 self.entries.contains_key(cid)
419 }
420
421 #[must_use]
423 #[inline]
424 pub fn get(&self, cid: &str) -> Option<&ContentEntry> {
425 self.entries.get(cid)
426 }
427
428 pub fn clear(&mut self) {
430 self.entries.clear();
431 self.access_order.clear();
432 self.stats.total_entries = 0;
433 self.stats.total_bytes = 0;
434 }
435
436 pub fn set_policy(&mut self, policy: ExpirationPolicy) {
438 self.policy = policy;
439 }
440}
441
442fn current_timestamp_ms() -> u64 {
444 SystemTime::now()
445 .duration_since(UNIX_EPOCH)
446 .unwrap_or_default()
447 .as_millis() as u64
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use std::thread::sleep;
454
455 #[test]
456 fn test_content_entry_new() {
457 let entry = ContentEntry::new("test:123".to_string(), 1024);
458 assert_eq!(entry.cid, "test:123");
459 assert_eq!(entry.size_bytes, 1024);
460 assert_eq!(entry.access_count, 0);
461 }
462
463 #[test]
464 fn test_content_entry_access() {
465 let mut entry = ContentEntry::new("test:123".to_string(), 1024);
466 entry.record_access();
467 assert_eq!(entry.access_count, 1);
468 }
469
470 #[test]
471 fn test_expiration_policy_ttl() {
472 let policy = ExpirationPolicy::ttl(Duration::from_millis(100));
473 let entry = ContentEntry::new("test:123".to_string(), 1024);
474
475 assert!(!policy.should_expire(&entry));
477
478 sleep(Duration::from_millis(150));
480 assert!(policy.should_expire(&entry));
481 }
482
483 #[test]
484 fn test_expiration_policy_idle_timeout() {
485 let policy = ExpirationPolicy::idle_timeout(Duration::from_millis(100));
486 let mut entry = ContentEntry::new("test:123".to_string(), 1024);
487
488 sleep(Duration::from_millis(150));
489 assert!(policy.should_expire(&entry));
490
491 entry.record_access();
493 assert!(!policy.should_expire(&entry));
494 }
495
496 #[test]
497 fn test_expiration_manager_register() {
498 let policy = ExpirationPolicy::Never;
499 let mut manager = ExpirationManager::new(policy);
500
501 manager.register("test:123".to_string(), 1024);
502
503 assert_eq!(manager.entry_count(), 1);
504 assert_eq!(manager.total_bytes(), 1024);
505 assert!(manager.contains("test:123"));
506 }
507
508 #[test]
509 fn test_expiration_manager_expire_ttl() {
510 let policy = ExpirationPolicy::ttl(Duration::from_millis(100));
511 let mut manager = ExpirationManager::new(policy);
512
513 manager.register("test:123".to_string(), 1024);
514
515 let expired = manager.get_expired();
517 assert_eq!(expired.len(), 0);
518
519 sleep(Duration::from_millis(150));
521 let expired = manager.expire();
522 assert_eq!(expired.len(), 1);
523 assert_eq!(expired[0], "test:123");
524 assert_eq!(manager.entry_count(), 0);
525 }
526
527 #[test]
528 fn test_expiration_manager_lru() {
529 let policy = ExpirationPolicy::lru(2);
530 let mut manager = ExpirationManager::new(policy);
531
532 manager.register("test:1".to_string(), 1024);
533 manager.register("test:2".to_string(), 1024);
534 manager.register("test:3".to_string(), 1024);
535
536 let expired = manager.expire();
538 assert_eq!(expired.len(), 1);
539 assert_eq!(expired[0], "test:1");
540 assert_eq!(manager.entry_count(), 2);
541 }
542
543 #[test]
544 fn test_expiration_manager_size_quota() {
545 let policy = ExpirationPolicy::size_quota(2000);
546 let mut manager = ExpirationManager::new(policy);
547
548 manager.register("test:1".to_string(), 1000);
549 manager.register("test:2".to_string(), 1000);
550 manager.register("test:3".to_string(), 1000);
551
552 let expired = manager.expire();
554 assert!(!expired.is_empty());
555 assert!(manager.total_bytes() <= 2000);
556 }
557
558 #[test]
559 fn test_expiration_manager_record_access() {
560 let policy = ExpirationPolicy::Never;
561 let mut manager = ExpirationManager::new(policy);
562
563 manager.register("test:123".to_string(), 1024);
564 manager.record_access("test:123");
565
566 let entry = manager.get("test:123").unwrap();
567 assert_eq!(entry.access_count, 1);
568 }
569
570 #[test]
571 fn test_expiration_manager_remove() {
572 let policy = ExpirationPolicy::Never;
573 let mut manager = ExpirationManager::new(policy);
574
575 manager.register("test:123".to_string(), 1024);
576 assert_eq!(manager.entry_count(), 1);
577
578 let removed = manager.remove("test:123");
579 assert!(removed.is_some());
580 assert_eq!(manager.entry_count(), 0);
581 }
582
583 #[test]
584 fn test_expiration_manager_stats() {
585 let policy = ExpirationPolicy::ttl(Duration::from_millis(100));
586 let mut manager = ExpirationManager::new(policy);
587
588 manager.register("test:1".to_string(), 1024);
589 manager.register("test:2".to_string(), 2048);
590
591 sleep(Duration::from_millis(150));
592 let _expired = manager.expire();
593
594 let stats = manager.stats();
595 assert_eq!(stats.expired_count, 2);
596 assert_eq!(stats.bytes_freed, 3072);
597 }
598
599 #[test]
600 fn test_expiration_manager_batch() {
601 let policy = ExpirationPolicy::ttl(Duration::from_millis(100));
602 let mut manager = ExpirationManager::new(policy);
603
604 for i in 0..10 {
605 manager.register(format!("test:{i}"), 1024);
606 }
607
608 sleep(Duration::from_millis(150));
609
610 let batch1 = manager.expire_batch(3);
612 assert_eq!(batch1.len(), 3);
613
614 let batch2 = manager.expire_batch(3);
615 assert_eq!(batch2.len(), 3);
616 }
617
618 #[test]
619 fn test_expiration_manager_max_entries() {
620 let policy = ExpirationPolicy::Never;
621 let mut manager = ExpirationManager::with_max_entries(policy, 5);
622
623 for i in 0..10 {
624 manager.register(format!("test:{i}"), 1024);
625 }
626
627 assert_eq!(manager.entry_count(), 5);
629 }
630
631 #[test]
632 fn test_explicit_expiration() {
633 let policy = ExpirationPolicy::Never;
634 let mut manager = ExpirationManager::new(policy);
635
636 let expires_at = Instant::now() + Duration::from_millis(100);
637 manager.register_with_expiration("test:123".to_string(), 1024, expires_at);
638
639 let expired = manager.get_expired();
641 assert_eq!(expired.len(), 0);
642
643 sleep(Duration::from_millis(150));
645 let expired = manager.expire();
646 assert_eq!(expired.len(), 1);
647 }
648}