|
7 | 7 | //! 3. Repacks segment files whose garbage ratio exceeds 5%. |
8 | 8 |
|
9 | 9 | use crate::Result; |
| 10 | +use crate::catalog::codec::{Catalog, CatalogRowKind, SegmentMeta}; |
10 | 11 | use crate::errors::PagedbError; |
11 | 12 | use crate::segment::reader::SegmentReader; |
12 | 13 | use crate::segment::types::SegmentPageKind; |
13 | 14 | use crate::segment::writer::SegmentWriter; |
14 | | -use crate::txn::db::Db; |
| 15 | +use crate::txn::db::{Db, WriterState}; |
15 | 16 | use crate::vfs::{Vfs, VfsFile}; |
16 | 17 |
|
17 | | -use super::helpers::{find_segment_name_inner, list_all_segments_inner, replace_segment_compact}; |
| 18 | +use super::helpers::{SEGMENT_ROW_BATCH, replace_segment_compact, segment_rows_from}; |
18 | 19 | use super::types::CompactStats; |
19 | 20 |
|
20 | 21 | /// Full online compaction. See module-level docs for the staged flow. |
@@ -81,66 +82,104 @@ async fn compact_now_inner<V: Vfs + Clone>(db: &Db<V>) -> Result<CompactStats> { |
81 | 82 | } |
82 | 83 |
|
83 | 84 | // ── 3. Repack segments ──────────────────────────────────────────────────── |
84 | | - let all_segments = list_all_segments_inner(&db.pager, db.realm_id, &state).await?; |
85 | | - for meta in all_segments { |
86 | | - let live = crate::segment::writer::live_path(&meta.segment_id); |
87 | | - let file_size = db |
88 | | - .vfs |
89 | | - .open(&live, crate::vfs::types::OpenMode::Read) |
90 | | - .await? |
91 | | - .len() |
92 | | - .await?; |
93 | | - // Skip segments with < 5% garbage. |
94 | | - let threshold = meta.total_bytes.saturating_add(meta.total_bytes / 20); |
95 | | - if file_size <= threshold { |
96 | | - continue; |
| 85 | + // The catalog is streamed in bounded batches rather than listed: a repack |
| 86 | + // must not size an allocation by how many segments the embedder has linked. |
| 87 | + // Each replacement rewrites the row under its own key, so the cursor stays |
| 88 | + // valid across the mutation and resumes on `key ‖ 0x00`. |
| 89 | + let segment_prefix = [CatalogRowKind::Segment as u8]; |
| 90 | + let mut cursor: Vec<u8> = segment_prefix.to_vec(); |
| 91 | + loop { |
| 92 | + let batch = segment_rows_from(&db.pager, db.realm_id, &state, &cursor).await?; |
| 93 | + let Some((last_key, _)) = batch.last() else { |
| 94 | + break; |
| 95 | + }; |
| 96 | + cursor.clear(); |
| 97 | + cursor.extend_from_slice(last_key); |
| 98 | + cursor.push(0); |
| 99 | + let exhausted = batch.len() < SEGMENT_ROW_BATCH; |
| 100 | + |
| 101 | + for (key, meta) in batch { |
| 102 | + repack_one_segment(db, &mut state, &visibility_guard, &key, &meta, &mut result).await?; |
97 | 103 | } |
98 | 104 |
|
99 | | - let mmap_limit = u64::try_from(db.options.mmap_view_scratch_bytes).unwrap_or(u64::MAX); |
100 | | - let reader = SegmentReader::open_internal( |
101 | | - db.pager.clone(), |
102 | | - meta.clone(), |
103 | | - db.mmap_bytes_in_use.clone(), |
104 | | - mmap_limit, |
105 | | - ) |
106 | | - .await?; |
107 | | - db.vfs.mkdir_all("seg/.staging").await?; |
108 | | - let new_segment_id = crate::crypto::random::segment_id()?; |
109 | | - let mut writer = SegmentWriter::create_internal( |
110 | | - db.pager.clone(), |
111 | | - meta.realm_id, |
112 | | - new_segment_id, |
113 | | - db.file_id, |
114 | | - meta.segment_kind, |
115 | | - ) |
| 105 | + if exhausted { |
| 106 | + break; |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + Ok(result) |
| 111 | +} |
| 112 | + |
| 113 | +/// Repack one catalog-linked segment when its file carries more than 5% garbage, |
| 114 | +/// republishing the replacement under the row's own name. |
| 115 | +async fn repack_one_segment<V: Vfs + Clone>( |
| 116 | + db: &Db<V>, |
| 117 | + state: &mut WriterState, |
| 118 | + visibility_guard: &tokio::sync::RwLockWriteGuard<'_, ()>, |
| 119 | + key: &[u8], |
| 120 | + meta: &SegmentMeta, |
| 121 | + result: &mut CompactStats, |
| 122 | +) -> Result<()> { |
| 123 | + let live = crate::segment::writer::live_path(&meta.segment_id); |
| 124 | + let file_size = db |
| 125 | + .vfs |
| 126 | + .open(&live, crate::vfs::types::OpenMode::Read) |
| 127 | + .await? |
| 128 | + .len() |
116 | 129 | .await?; |
| 130 | + // Skip segments with < 5% garbage. |
| 131 | + let threshold = meta.total_bytes.saturating_add(meta.total_bytes / 20); |
| 132 | + if file_size <= threshold { |
| 133 | + return Ok(()); |
| 134 | + } |
| 135 | + |
| 136 | + // The row key is `[kind] || realm_id || name`, so the name the replacement |
| 137 | + // must be relinked under comes straight from the row that carried this |
| 138 | + // meta. Validating it against the meta rejects a malformed key before the |
| 139 | + // repack republishes anything. |
| 140 | + let seg_name = String::from_utf8_lossy(Catalog::validate_segment_key(key, meta)?).into_owned(); |
| 141 | + |
| 142 | + let mmap_limit = u64::try_from(db.options.mmap_view_scratch_bytes).unwrap_or(u64::MAX); |
| 143 | + let reader = SegmentReader::open_internal( |
| 144 | + db.pager.clone(), |
| 145 | + meta.clone(), |
| 146 | + db.mmap_bytes_in_use.clone(), |
| 147 | + mmap_limit, |
| 148 | + ) |
| 149 | + .await?; |
| 150 | + db.vfs.mkdir_all("seg/.staging").await?; |
| 151 | + let new_segment_id = crate::crypto::random::segment_id()?; |
| 152 | + let mut writer = SegmentWriter::create_internal( |
| 153 | + db.pager.clone(), |
| 154 | + meta.realm_id, |
| 155 | + new_segment_id, |
| 156 | + db.file_id, |
| 157 | + meta.segment_kind, |
| 158 | + ) |
| 159 | + .await?; |
117 | 160 |
|
118 | | - for page_id in 1..meta.page_count.saturating_sub(1) { |
119 | | - match reader.read_page(page_id).await { |
120 | | - Ok(page_bytes) => { |
121 | | - writer |
122 | | - .append_page(SegmentPageKind::Data, &page_bytes) |
123 | | - .await?; |
124 | | - } |
125 | | - Err(PagedbError::NotFound) => {} |
126 | | - Err(e) => return Err(e), |
| 161 | + for page_id in 1..meta.page_count.saturating_sub(1) { |
| 162 | + match reader.read_page(page_id).await { |
| 163 | + Ok(page_bytes) => { |
| 164 | + writer |
| 165 | + .append_page(SegmentPageKind::Data, &page_bytes) |
| 166 | + .await?; |
127 | 167 | } |
| 168 | + Err(PagedbError::NotFound) => {} |
| 169 | + Err(e) => return Err(e), |
128 | 170 | } |
129 | | - |
130 | | - let new_meta = writer.seal().await?; |
131 | | - let seg_name = |
132 | | - find_segment_name_inner(&db.pager, db.realm_id, &state, &meta.segment_id).await?; |
133 | | - replace_segment_compact( |
134 | | - db, |
135 | | - &mut state, |
136 | | - &visibility_guard, |
137 | | - &seg_name, |
138 | | - &meta.segment_id, |
139 | | - &new_meta, |
140 | | - ) |
141 | | - .await?; |
142 | | - result.segments_repacked += 1; |
143 | 171 | } |
144 | 172 |
|
145 | | - Ok(result) |
| 173 | + let new_meta = writer.seal().await?; |
| 174 | + replace_segment_compact( |
| 175 | + db, |
| 176 | + state, |
| 177 | + visibility_guard, |
| 178 | + &seg_name, |
| 179 | + &meta.segment_id, |
| 180 | + &new_meta, |
| 181 | + ) |
| 182 | + .await?; |
| 183 | + result.segments_repacked += 1; |
| 184 | + Ok(()) |
146 | 185 | } |
0 commit comments