1#![allow(dead_code)]
2use std::collections::HashMap;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ProxyStage {
13 Active,
15 Idle,
17 Stale,
19 Expired,
21 Archived,
23 Deleted,
25}
26
27impl ProxyStage {
28 pub fn label(&self) -> &'static str {
30 match self {
31 Self::Active => "Active",
32 Self::Idle => "Idle",
33 Self::Stale => "Stale",
34 Self::Expired => "Expired",
35 Self::Archived => "Archived",
36 Self::Deleted => "Deleted",
37 }
38 }
39
40 pub fn is_usable(&self) -> bool {
42 matches!(self, Self::Active | Self::Idle)
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct AgingPolicy {
49 pub idle_after_days: u64,
51 pub stale_after_days: u64,
53 pub expire_after_days: u64,
55 pub auto_archive: bool,
57 pub auto_delete: bool,
59 pub min_size_bytes: u64,
61}
62
63impl Default for AgingPolicy {
64 fn default() -> Self {
65 Self {
66 idle_after_days: 7,
67 stale_after_days: 30,
68 expire_after_days: 90,
69 auto_archive: true,
70 auto_delete: false,
71 min_size_bytes: 1024,
72 }
73 }
74}
75
76impl AgingPolicy {
77 pub fn strict() -> Self {
79 Self {
80 idle_after_days: 3,
81 stale_after_days: 14,
82 expire_after_days: 30,
83 auto_archive: true,
84 auto_delete: true,
85 min_size_bytes: 0,
86 }
87 }
88
89 pub fn relaxed() -> Self {
91 Self {
92 idle_after_days: 30,
93 stale_after_days: 180,
94 expire_after_days: 365,
95 auto_archive: false,
96 auto_delete: false,
97 min_size_bytes: 0,
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct ProxyRecord {
105 pub id: String,
107 pub path: String,
109 pub size_bytes: u64,
111 pub created_day: u64,
113 pub last_access_day: u64,
115 pub access_count: u64,
117 pub stage: ProxyStage,
119}
120
121impl ProxyRecord {
122 pub fn new(id: &str, path: &str, size_bytes: u64, created_day: u64) -> Self {
124 Self {
125 id: id.to_string(),
126 path: path.to_string(),
127 size_bytes,
128 created_day,
129 last_access_day: created_day,
130 access_count: 0,
131 stage: ProxyStage::Active,
132 }
133 }
134
135 pub fn record_access(&mut self, day: u64) {
137 self.last_access_day = day;
138 self.access_count += 1;
139 if self.stage.is_usable() || self.stage == ProxyStage::Stale {
141 self.stage = ProxyStage::Active;
142 }
143 }
144
145 pub fn days_since_access(&self, current_day: u64) -> u64 {
147 current_day.saturating_sub(self.last_access_day)
148 }
149
150 pub fn age_days(&self, current_day: u64) -> u64 {
152 current_day.saturating_sub(self.created_day)
153 }
154}
155
156#[derive(Debug, Clone)]
158pub struct AgingSweepResult {
159 pub newly_idle: usize,
161 pub newly_stale: usize,
163 pub newly_expired: usize,
165 pub archived: usize,
167 pub deleted: usize,
169 pub bytes_reclaimed: u64,
171}
172
173impl AgingSweepResult {
174 pub fn total_transitions(&self) -> usize {
176 self.newly_idle + self.newly_stale + self.newly_expired + self.archived + self.deleted
177 }
178}
179
180pub struct AgingManager {
182 policy: AgingPolicy,
184 records: HashMap<String, ProxyRecord>,
186}
187
188impl AgingManager {
189 pub fn new(policy: AgingPolicy) -> Self {
191 Self {
192 policy,
193 records: HashMap::new(),
194 }
195 }
196
197 pub fn add_record(&mut self, record: ProxyRecord) {
199 self.records.insert(record.id.clone(), record);
200 }
201
202 pub fn get_record(&self, id: &str) -> Option<&ProxyRecord> {
204 self.records.get(id)
205 }
206
207 pub fn record_access(&mut self, id: &str, day: u64) -> bool {
209 if let Some(record) = self.records.get_mut(id) {
210 record.record_access(day);
211 true
212 } else {
213 false
214 }
215 }
216
217 pub fn record_count(&self) -> usize {
219 self.records.len()
220 }
221
222 pub fn total_size_bytes(&self) -> u64 {
224 self.records.values().map(|r| r.size_bytes).sum()
225 }
226
227 pub fn sweep(&mut self, current_day: u64) -> AgingSweepResult {
229 let mut result = AgingSweepResult {
230 newly_idle: 0,
231 newly_stale: 0,
232 newly_expired: 0,
233 archived: 0,
234 deleted: 0,
235 bytes_reclaimed: 0,
236 };
237
238 let mut to_delete = Vec::new();
239
240 for record in self.records.values_mut() {
241 if record.size_bytes < self.policy.min_size_bytes {
242 continue;
243 }
244
245 let days_inactive = record.days_since_access(current_day);
246 let old_stage = record.stage;
247
248 let new_stage = if days_inactive >= self.policy.expire_after_days {
250 ProxyStage::Expired
251 } else if days_inactive >= self.policy.stale_after_days {
252 ProxyStage::Stale
253 } else if days_inactive >= self.policy.idle_after_days {
254 ProxyStage::Idle
255 } else {
256 ProxyStage::Active
257 };
258
259 if new_stage as u8 > old_stage as u8 || old_stage == ProxyStage::Active {
261 match new_stage {
262 ProxyStage::Idle if old_stage == ProxyStage::Active => {
263 record.stage = ProxyStage::Idle;
264 result.newly_idle += 1;
265 }
266 ProxyStage::Stale
267 if old_stage == ProxyStage::Active || old_stage == ProxyStage::Idle =>
268 {
269 if self.policy.auto_archive {
270 record.stage = ProxyStage::Archived;
271 result.archived += 1;
272 result.bytes_reclaimed += record.size_bytes;
273 } else {
274 record.stage = ProxyStage::Stale;
275 result.newly_stale += 1;
276 }
277 }
278 ProxyStage::Expired
279 if old_stage != ProxyStage::Expired && old_stage != ProxyStage::Deleted =>
280 {
281 if self.policy.auto_delete {
282 to_delete.push(record.id.clone());
283 result.deleted += 1;
284 result.bytes_reclaimed += record.size_bytes;
285 } else {
286 record.stage = ProxyStage::Expired;
287 result.newly_expired += 1;
288 }
289 }
290 _ => {}
291 }
292 }
293 }
294
295 for id in &to_delete {
296 self.records.remove(id);
297 }
298
299 result
300 }
301
302 pub fn records_in_stage(&self, stage: ProxyStage) -> Vec<&ProxyRecord> {
304 self.records.values().filter(|r| r.stage == stage).collect()
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 fn make_record(id: &str, size: u64, created_day: u64) -> ProxyRecord {
313 ProxyRecord::new(id, &format!("/proxy/{id}.mp4"), size, created_day)
314 }
315
316 #[test]
317 fn test_proxy_stage_labels() {
318 assert_eq!(ProxyStage::Active.label(), "Active");
319 assert_eq!(ProxyStage::Expired.label(), "Expired");
320 assert_eq!(ProxyStage::Deleted.label(), "Deleted");
321 }
322
323 #[test]
324 fn test_proxy_stage_usable() {
325 assert!(ProxyStage::Active.is_usable());
326 assert!(ProxyStage::Idle.is_usable());
327 assert!(!ProxyStage::Stale.is_usable());
328 assert!(!ProxyStage::Expired.is_usable());
329 assert!(!ProxyStage::Archived.is_usable());
330 }
331
332 #[test]
333 fn test_policy_defaults() {
334 let policy = AgingPolicy::default();
335 assert_eq!(policy.idle_after_days, 7);
336 assert_eq!(policy.stale_after_days, 30);
337 assert_eq!(policy.expire_after_days, 90);
338 }
339
340 #[test]
341 fn test_policy_strict() {
342 let policy = AgingPolicy::strict();
343 assert!(policy.auto_delete);
344 assert!(policy.expire_after_days < AgingPolicy::default().expire_after_days);
345 }
346
347 #[test]
348 fn test_policy_relaxed() {
349 let policy = AgingPolicy::relaxed();
350 assert!(!policy.auto_delete);
351 assert!(policy.expire_after_days > AgingPolicy::default().expire_after_days);
352 }
353
354 #[test]
355 fn test_record_access_reactivates() {
356 let mut rec = make_record("a", 1000, 0);
357 rec.stage = ProxyStage::Stale;
358 rec.record_access(50);
359 assert_eq!(rec.stage, ProxyStage::Active);
360 assert_eq!(rec.last_access_day, 50);
361 assert_eq!(rec.access_count, 1);
362 }
363
364 #[test]
365 fn test_record_days_since_access() {
366 let rec = make_record("a", 1000, 10);
367 assert_eq!(rec.days_since_access(25), 15);
368 }
369
370 #[test]
371 fn test_record_age_days() {
372 let rec = make_record("a", 1000, 10);
373 assert_eq!(rec.age_days(50), 40);
374 }
375
376 #[test]
377 fn test_manager_add_and_get() {
378 let mut mgr = AgingManager::new(AgingPolicy::default());
379 mgr.add_record(make_record("a", 5000, 0));
380 assert_eq!(mgr.record_count(), 1);
381 assert!(mgr.get_record("a").is_some());
382 assert!(mgr.get_record("b").is_none());
383 }
384
385 #[test]
386 fn test_manager_total_size() {
387 let mut mgr = AgingManager::new(AgingPolicy::default());
388 mgr.add_record(make_record("a", 5000, 0));
389 mgr.add_record(make_record("b", 3000, 0));
390 assert_eq!(mgr.total_size_bytes(), 8000);
391 }
392
393 #[test]
394 fn test_sweep_idle_transition() {
395 let mut mgr = AgingManager::new(AgingPolicy::default());
396 mgr.add_record(make_record("a", 5000, 0));
397 let result = mgr.sweep(10);
399 assert_eq!(result.newly_idle, 1);
400 assert_eq!(
401 mgr.get_record("a").expect("should succeed in test").stage,
402 ProxyStage::Idle
403 );
404 }
405
406 #[test]
407 fn test_sweep_auto_archive() {
408 let mut policy = AgingPolicy::default();
409 policy.auto_archive = true;
410 let mut mgr = AgingManager::new(policy);
411 mgr.add_record(make_record("a", 5000, 0));
412 let result = mgr.sweep(35);
414 assert_eq!(result.archived, 1);
415 assert_eq!(
416 mgr.get_record("a").expect("should succeed in test").stage,
417 ProxyStage::Archived
418 );
419 }
420
421 #[test]
422 fn test_sweep_auto_delete() {
423 let policy = AgingPolicy::strict();
424 let mut mgr = AgingManager::new(policy);
425 mgr.add_record(make_record("a", 5000, 0));
426 let result = mgr.sweep(35);
428 assert_eq!(result.deleted, 1);
429 assert!(mgr.get_record("a").is_none());
430 }
431
432 #[test]
433 fn test_sweep_skips_small_files() {
434 let mut policy = AgingPolicy::default();
435 policy.min_size_bytes = 10_000;
436 let mut mgr = AgingManager::new(policy);
437 mgr.add_record(make_record("tiny", 500, 0));
438 let result = mgr.sweep(100);
439 assert_eq!(result.total_transitions(), 0);
441 assert_eq!(
442 mgr.get_record("tiny")
443 .expect("should succeed in test")
444 .stage,
445 ProxyStage::Active
446 );
447 }
448
449 #[test]
450 fn test_records_in_stage() {
451 let mut mgr = AgingManager::new(AgingPolicy::default());
452 mgr.add_record(make_record("a", 5000, 0));
453 mgr.add_record(make_record("b", 5000, 0));
454 let active = mgr.records_in_stage(ProxyStage::Active);
455 assert_eq!(active.len(), 2);
456 }
457
458 #[test]
459 fn test_record_access_through_manager() {
460 let mut mgr = AgingManager::new(AgingPolicy::default());
461 mgr.add_record(make_record("a", 5000, 0));
462 assert!(mgr.record_access("a", 5));
463 assert!(!mgr.record_access("nonexistent", 5));
464 assert_eq!(
465 mgr.get_record("a")
466 .expect("should succeed in test")
467 .access_count,
468 1
469 );
470 }
471}