Skip to main content

mongreldb_core/
gc.rs

1//! Operational tools: garbage collection and integrity check.
2//!
3//! `gc` removes run files not referenced by the manifest (orphans from aborted
4//! flushes / old compaction inputs) and stale WAL segments (all but the active
5//! one). `check` re-verifies every referenced run's footer checksum.
6
7use crate::engine::Table;
8use crate::sorted_run::read_header;
9use crate::Result;
10use std::collections::HashSet;
11
12/// Outcome of a [`Table::gc`] pass.
13#[derive(Debug, Default, Clone)]
14pub struct GcReport {
15    pub runs_removed: usize,
16    pub wal_segments_removed: usize,
17    pub bytes_freed: u64,
18}
19
20/// Outcome of a [`Table::check`] pass.
21#[derive(Debug, Default, Clone)]
22pub struct CheckReport {
23    pub runs_checked: usize,
24    pub runs_ok: usize,
25    pub issues: Vec<String>,
26}
27
28/// Outcome of a [`Table::doctor`] pass (best-effort repair): corrupt runs are
29/// dropped from the manifest so the table is usable again, at the cost of the
30/// data they held.
31#[derive(Debug, Default, Clone)]
32pub struct DoctorReport {
33    pub runs_dropped: Vec<u128>,
34}
35
36impl Table {
37    /// Remove orphan run files (not in the manifest) and all but the active WAL
38    /// segment. Safe to run any time; readers pin snapshots, not files.
39    pub fn gc(&self) -> Result<GcReport> {
40        let mut report = GcReport::default();
41        let live: HashSet<u128> = self
42            .run_refs()
43            .iter()
44            .map(|r| r.run_id)
45            .chain(self.retiring_run_ids())
46            .collect();
47
48        for entry in std::fs::read_dir(self.runs_dir())? {
49            let entry = entry?;
50            let path = entry.path();
51            let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
52                continue;
53            };
54            let Some(id) = name
55                .strip_prefix("r-")
56                .and_then(|s| s.strip_suffix(".sr"))
57                .and_then(|s| s.parse::<u128>().ok())
58            else {
59                continue;
60            };
61            if !live.contains(&id) {
62                let bytes = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
63                if std::fs::remove_file(&path).is_ok() {
64                    report.runs_removed += 1;
65                    report.bytes_freed += bytes;
66                }
67            }
68        }
69
70        // WAL: keep only the highest-numbered segment.
71        let mut segs: Vec<(u32, std::path::PathBuf)> = Vec::new();
72        for entry in std::fs::read_dir(self.wal_dir())? {
73            let entry = entry?;
74            let path = entry.path();
75            let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
76                continue;
77            };
78            let Some(n) = name
79                .strip_prefix("seg-")
80                .and_then(|s| s.strip_suffix(".wal"))
81                .and_then(|s| s.parse::<u32>().ok())
82            else {
83                continue;
84            };
85            segs.push((n, path));
86        }
87        segs.sort_by_key(|(n, _)| *n);
88        if let Some((_, active)) = segs.last() {
89            let active = active.clone();
90            for (_, path) in segs {
91                if path != active {
92                    let bytes = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
93                    if std::fs::remove_file(&path).is_ok() {
94                        report.wal_segments_removed += 1;
95                        report.bytes_freed += bytes;
96                    }
97                }
98            }
99        }
100        Ok(report)
101    }
102
103    /// Verify every manifest-referenced run's footer checksum.
104    pub fn check(&self) -> Result<CheckReport> {
105        let mut report = CheckReport::default();
106        for rr in self.run_refs() {
107            report.runs_checked += 1;
108            let path = self.run_path(rr.run_id as u64);
109            match read_header(&path) {
110                Ok(_) => report.runs_ok += 1,
111                Err(e) => report.issues.push(format!("run {}: {e}", rr.run_id)),
112            }
113        }
114        Ok(report)
115    }
116
117    /// Best-effort repair: drop any manifest-referenced run that fails its
118    /// checksum, so the table reopens in a consistent (if lossy) state.
119    pub fn doctor(&mut self) -> Result<DoctorReport> {
120        let mut report = DoctorReport::default();
121        let live: Vec<u128> = self.run_refs().iter().map(|r| r.run_id).collect();
122        let mut kept: Vec<crate::manifest::RunRef> = Vec::new();
123        for id in live {
124            let path = self.run_path(id as u64);
125            if read_header(&path).is_ok() {
126                if let Some(rr) = self.run_refs().iter().find(|r| r.run_id == id).cloned() {
127                    kept.push(rr);
128                }
129            } else {
130                report.runs_dropped.push(id);
131            }
132        }
133        self.set_run_refs(kept);
134        self.persist_manifest(self.current_epoch())?;
135        // If any run was dropped, the checkpoint now references row-ids whose
136        // data is gone → invalidate it so reopen rebuilds from survivors.
137        if !report.runs_dropped.is_empty() {
138            self.invalidate_index_checkpoint();
139        }
140        Ok(report)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
148    use crate::Value;
149    use tempfile::tempdir;
150
151    fn schema() -> Schema {
152        Schema {
153            schema_id: 1,
154            columns: vec![ColumnDef {
155                id: 1,
156                name: "v".into(),
157                ty: TypeId::Int64,
158                flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
159                default_value: None,
160            }],
161            indexes: Vec::new(),
162            colocation: vec![],
163            constraints: Default::default(),
164            clustered: false,
165        }
166    }
167
168    #[test]
169    fn gc_removes_orphan_runs_and_old_wal_segments() {
170        let dir = tempdir().unwrap();
171        let mut db = Table::create(dir.path(), schema(), 1).unwrap();
172        db.set_mutable_run_spill_bytes(1); // spill every flush to a run
173        db.put(vec![(1, Value::Int64(1))]).unwrap();
174        db.flush().unwrap(); // run 1 + rotated WAL
175                             // Orphan run file.
176        std::fs::write(dir.path().join("_runs").join("r-777.sr"), b"junk").unwrap();
177
178        let report = db.gc().unwrap();
179        assert!(report.runs_removed >= 1, "orphan run removed");
180        assert!(!dir.path().join("_runs").join("r-777.sr").exists());
181
182        // check should still pass for the live run.
183        let check = db.check().unwrap();
184        assert_eq!(check.runs_checked, 1);
185        assert!(check.issues.is_empty(), "{:?}", check.issues);
186    }
187
188    #[test]
189    fn check_flags_a_corrupt_run() {
190        let dir = tempdir().unwrap();
191        let mut db = Table::create(dir.path(), schema(), 1).unwrap();
192        db.set_mutable_run_spill_bytes(1); // spill every flush to a run
193        db.put(vec![(1, Value::Int64(1))]).unwrap();
194        db.flush().unwrap();
195        // Corrupt the live run.
196        let live = dir.path().join("_runs").join("r-1.sr");
197        let mut bytes = std::fs::read(&live).unwrap();
198        bytes[300] ^= 0xFF;
199        std::fs::write(&live, bytes).unwrap();
200
201        let check = db.check().unwrap();
202        assert_eq!(check.runs_checked, 1);
203        assert!(!check.issues.is_empty(), "corruption must be flagged");
204    }
205}