1use crate::engine::Table;
8use crate::sorted_run::read_header;
9use crate::Result;
10use std::collections::HashSet;
11
12#[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#[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#[derive(Debug, Default, Clone)]
32pub struct DoctorReport {
33 pub runs_dropped: Vec<u128>,
34}
35
36impl Table {
37 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 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 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 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 !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); db.put(vec![(1, Value::Int64(1))]).unwrap();
174 db.flush().unwrap(); 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 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); db.put(vec![(1, Value::Int64(1))]).unwrap();
194 db.flush().unwrap();
195 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}