1use std::path::PathBuf;
2use crate::{Result, Wal};
3
4#[derive(Debug)]
5pub struct CleanupResult {
6 pub deleted: Vec<DeletedSegment>,
7 pub live_count: u64,
8 pub bytes_reclaimed: u64,
9}
10
11#[derive(Debug)]
12pub struct DeletedSegment {
13 pub path: PathBuf,
14 pub expiration_ms: i64,
15 pub file_size: u64,
16}
17
18impl Wal {
19 pub fn cleanup(&self) -> Result<CleanupResult> {
20 let now_ms = std::time::SystemTime::now()
21 .duration_since(std::time::UNIX_EPOCH)
22 .unwrap_or_default()
23 .as_millis() as i64;
24
25 let mut deleted = Vec::new();
26 let mut live_count = 0u64;
27 let mut bytes_reclaimed = 0u64;
28
29 let entries = match std::fs::read_dir(self.dir()) {
30 Ok(e) => e,
31 Err(_) => return Ok(CleanupResult { deleted, live_count, bytes_reclaimed }),
32 };
33
34 for entry in entries.flatten() {
35 let filename = match entry.file_name().into_string() {
36 Ok(f) => f,
37 Err(_) => continue,
38 };
39
40 let expiration_ms = match self.parse_segment_filename(&filename) {
41 Some(exp) => exp,
42 None => continue,
43 };
44
45 if expiration_ms <= now_ms {
46 let file_size = entry.metadata().map(|m| m.len()).unwrap_or(0);
47 let path = entry.path();
48 if std::fs::remove_file(&path).is_ok() {
49 bytes_reclaimed += file_size;
50 deleted.push(DeletedSegment { path, expiration_ms, file_size });
51 } else {
52 live_count += 1;
53 }
54 } else {
55 live_count += 1;
56 }
57 }
58
59 Ok(CleanupResult { deleted, live_count, bytes_reclaimed })
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use crate::{Wal, WalOptions};
66 use tempfile::TempDir;
67 use std::time::Duration;
68
69 #[test]
70 fn test_cleanup_empty_dir() {
71 let dir = TempDir::new().unwrap();
72 let wal = Wal::new(dir.path(), "clean", WalOptions {
73 retention: Duration::from_secs(3600),
74 segment_duration: Duration::from_secs(600),
75 }).unwrap();
76 let result = wal.cleanup().unwrap();
77 assert_eq!(result.deleted.len(), 0);
78 assert_eq!(result.live_count, 0);
79 }
80
81 #[test]
82 fn test_cleanup_deletes_expired() {
83 let dir = TempDir::new().unwrap();
84 let wal = Wal::new(dir.path(), "exp", WalOptions {
85 retention: Duration::from_secs(3600),
86 segment_duration: Duration::from_secs(600),
87 }).unwrap();
88
89 let expired_path = dir.path().join("exp_1000.seg");
90 let mut header = b"NANO-LOG".to_vec();
91 header.extend_from_slice(&1000i64.to_le_bytes());
92 std::fs::write(&expired_path, &header).unwrap();
93
94 let result = wal.cleanup().unwrap();
95 assert_eq!(result.deleted.len(), 1);
96 assert_eq!(result.deleted[0].expiration_ms, 1000);
97 assert!(!expired_path.exists());
98 }
99
100 #[test]
101 fn test_cleanup_keeps_live_segments() {
102 let dir = TempDir::new().unwrap();
103 let wal = Wal::new(dir.path(), "live", WalOptions {
104 retention: Duration::from_secs(3600),
105 segment_duration: Duration::from_secs(600),
106 }).unwrap();
107
108 let live_path = dir.path().join("live_99999999999999.seg");
109 std::fs::write(&live_path, b"NANO-LOG\x00\x00\x00\x00\x00\x00\x00\x00").unwrap();
110
111 let result = wal.cleanup().unwrap();
112 assert_eq!(result.deleted.len(), 0);
113 assert_eq!(result.live_count, 1);
114 assert!(live_path.exists());
115 }
116
117 #[test]
118 fn test_cleanup_ignores_other_prefixes() {
119 let dir = TempDir::new().unwrap();
120 let wal = Wal::new(dir.path(), "mine", WalOptions {
121 retention: Duration::from_secs(3600),
122 segment_duration: Duration::from_secs(600),
123 }).unwrap();
124
125 let other_path = dir.path().join("other_1000.seg");
126 std::fs::write(&other_path, b"NANO-LOG\x00\x00\x00\x00\x00\x00\x00\x00").unwrap();
127
128 let result = wal.cleanup().unwrap();
129 assert_eq!(result.deleted.len(), 0);
130 assert!(other_path.exists());
131 }
132}