mongreldb_core/
compaction.rs1use crate::engine::Table;
8use crate::epoch::Epoch;
9use crate::manifest::RunRef;
10use crate::memtable::Row;
11use crate::sorted_run::RunWriter;
12use crate::Result;
13use std::collections::HashMap;
14
15impl Table {
16 pub const AUTO_COMPACT_RUN_THRESHOLD: usize = 8;
21
22 pub fn should_compact(&self) -> bool {
28 self.run_refs().len() >= Self::AUTO_COMPACT_RUN_THRESHOLD
29 || (self.ttl().is_some()
30 && !self.run_refs().is_empty()
31 && self.has_expired_run_rows().unwrap_or(false))
32 }
33
34 fn has_expired_run_rows(&self) -> Result<bool> {
35 let now_nanos = crate::engine::unix_nanos_now();
36 for run in self.run_refs() {
37 let mut reader = self.open_reader(run.run_id)?;
38 if reader
39 .all_rows()?
40 .iter()
41 .any(|row| self.row_expired_at(row, now_nanos))
42 {
43 return Ok(true);
44 }
45 }
46 Ok(false)
47 }
48
49 pub fn maybe_compact(&mut self) -> Result<bool> {
55 if !self.should_compact() {
56 return Ok(false);
57 }
58 self.compact()?;
59 Ok(true)
60 }
61
62 pub fn compact(&mut self) -> Result<()> {
67 let reclaim_ttl = self.ttl().is_some() && self.has_expired_run_rows()?;
68 if self.run_refs().len() < 2 && !reclaim_ttl {
69 return Ok(());
70 }
71 let min_active = self.min_active_snapshot();
72 let old_refs: Vec<RunRef> = self.run_refs().to_vec();
73 let now_nanos = crate::engine::unix_nanos_now();
74
75 let mutable_rows = if self.mutable_run_len() > 0 {
80 self.drain_mutable_run()
81 } else {
82 Vec::new()
83 };
84
85 let mut all: HashMap<u64, Vec<Row>> = HashMap::new();
87 for rr in &old_refs {
88 let mut reader = self.open_reader(rr.run_id)?;
89 for row in reader.all_rows()? {
90 all.entry(row.row_id.0).or_default().push(row);
91 }
92 }
93 for row in mutable_rows {
95 all.entry(row.row_id.0).or_default().push(row);
96 }
97
98 let mut rows: Vec<Row> = Vec::new();
99 let mut expired_reclaimed = false;
100 let mut current_live_count = 0u64;
101 for (_, mut vers) in all {
102 vers.sort_by_key(|r| r.committed_epoch);
103 let newest = vers.last().expect("at least one version");
104 let newest_epoch = newest.committed_epoch;
105 if !newest.deleted && !self.row_expired_at(newest, now_nanos) {
106 current_live_count += 1;
107 }
108 for row in select_keep(&vers, min_active) {
109 if self.row_expired_at(&row, now_nanos) {
110 expired_reclaimed = true;
111 if row.committed_epoch == newest_epoch
116 && min_active.is_some_and(|epoch| newest_epoch > epoch)
117 {
118 let mut tombstone = row;
119 tombstone.deleted = true;
120 tombstone.columns.clear();
121 rows.push(tombstone);
122 }
123 } else {
124 rows.push(row);
125 }
126 }
127 }
128 rows.sort_by_key(|r| (r.row_id, r.committed_epoch));
129
130 self.live_count = current_live_count;
133
134 let retire_epoch = self.current_epoch().0;
135 if rows.is_empty() {
136 self.set_run_refs(Vec::new());
142 for rr in &old_refs {
143 self.retire_run(rr.run_id, retire_epoch);
144 }
145 self.persist_manifest(self.current_epoch())?;
146 self.mark_indexes_incomplete();
149 return Ok(());
150 }
151
152 let run_id = self.next_run_id();
153 self.bump_next_run_id();
154 let path = self.run_path(run_id);
155 let kek = self.kek();
156 let mut writer = RunWriter::new(self.schema(), run_id as u128, self.current_epoch(), 1)
157 .clean(min_active.is_none())
158 .with_zstd_level(self.compaction_zstd_level());
159 if let Some(k) = &kek {
160 writer = writer.with_encryption(k.as_ref(), self.indexable_column_specs());
161 }
162 let header = writer.write(&path, &rows)?;
163
164 self.set_run_refs(vec![RunRef {
172 run_id: run_id as u128,
173 level: 1,
174 epoch_created: header.epoch_created,
175 row_count: header.row_count,
176 }]);
177 for rr in &old_refs {
178 self.retire_run(rr.run_id, retire_epoch);
179 }
180 self.persist_manifest(self.current_epoch())?;
181 self.build_learned_ranges()?;
185 self.clear_result_cache();
186 if expired_reclaimed {
187 self.mark_indexes_incomplete();
188 } else {
189 self.checkpoint_indexes(self.current_epoch());
190 }
191 Ok(())
192 }
193}
194
195fn select_keep(vers: &[Row], min_active: Option<Epoch>) -> Vec<Row> {
199 let newest = vers.last().expect("at least one version").clone();
200 match min_active {
201 None => {
202 if newest.deleted {
203 Vec::new()
204 } else {
205 vec![newest]
206 }
207 }
208 Some(min_e) => {
209 let recent_start = vers.partition_point(|row| row.committed_epoch < min_e);
210 let mut keep = vers[recent_start..].to_vec();
211 if recent_start > 0 && keep.first().map_or(true, |row| row.committed_epoch > min_e) {
212 let boundary = vers[recent_start - 1].clone();
213 if keep.is_empty() && boundary.deleted {
214 return Vec::new();
215 }
216 keep.insert(0, boundary);
217 }
218 keep
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
227 use crate::{Snapshot, Value};
228 use tempfile::tempdir;
229
230 fn schema() -> Schema {
231 Schema {
232 schema_id: 1,
233 columns: vec![ColumnDef {
234 id: 1,
235 name: "v".into(),
236 ty: TypeId::Int64,
237 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
238 default_value: None,
239 }],
240 indexes: Vec::new(),
241 colocation: vec![],
242 constraints: Default::default(),
243 clustered: false,
244 }
245 }
246
247 #[test]
248 fn compaction_merges_runs_and_gcs_tombstoned_row() {
249 let dir = tempdir().unwrap();
250 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
251 db.set_mutable_run_spill_bytes(1);
253 let mut ids = Vec::new();
254 for i in 1..=5i64 {
255 ids.push(db.put(vec![(1, Value::Int64(i))]).unwrap());
256 }
257 db.flush().unwrap();
258 db.delete(ids[2]).unwrap();
259 db.flush().unwrap();
260 db.put(vec![(1, Value::Int64(60))]).unwrap();
261 db.flush().unwrap();
262 assert_eq!(db.run_count(), 3);
263
264 db.compact().unwrap();
265 assert_eq!(db.run_count(), 1);
266 let rows = db.visible_rows(db.snapshot()).unwrap();
267 let row_ids: Vec<u64> = rows.iter().map(|r| r.row_id.0).collect();
268 assert!(!row_ids.contains(&ids[2].0), "tombstoned row must be GC'd");
269 assert_eq!(rows.len(), 5);
270 }
271
272 #[test]
273 fn pinned_snapshot_survives_compaction() {
274 let dir = tempdir().unwrap();
275 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
276 let r = db.put(vec![(1, Value::Int64(1))]).unwrap();
277 db.flush().unwrap(); let pinned = db.pin_snapshot();
281 assert_eq!(
282 db.get(r, pinned)
283 .and_then(|row| row.columns.get(&1).cloned()),
284 Some(Value::Int64(1))
285 );
286
287 db.delete(r).unwrap();
289 db.commit().unwrap();
290 db.flush().unwrap(); db.compact().unwrap(); assert_eq!(
295 db.get(r, pinned)
296 .and_then(|row| row.columns.get(&1).cloned()),
297 Some(Value::Int64(1))
298 );
299 assert_eq!(
301 db.get(r, db.snapshot())
302 .and_then(|row| row.columns.get(&1).cloned()),
303 None
304 );
305
306 db.unpin_snapshot(pinned);
308 db.compact().unwrap();
309 assert_eq!(
310 db.get(r, db.snapshot())
311 .and_then(|row| row.columns.get(&1).cloned()),
312 None
313 );
314 }
315
316 #[test]
317 fn _snapshot_import_used() {
318 let _ = Snapshot::at(Epoch(0));
319 }
320}