Skip to main content

nano_wal/
cleanup.rs

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}