1use std::path::{Path, PathBuf};
21use std::sync::Arc;
22
23use crate::value::{COLD_TAG_HASH, ColdRef, Value};
24use crate::{Store, key_heap_bytes_for, tier_codec};
25
26pub(crate) struct SegRows {
29 dir: PathBuf,
30 segs: Vec<(u32, SegSlot)>,
34 seq: u32,
35}
36
37struct SegSlot {
38 seg: Arc<kevy_seg::Seg>,
41 file: String,
42 live: u64,
43 dead: u64,
44}
45
46impl SegRows {
47 fn slot(&self, seq: u32) -> &SegSlot {
48 &self.segs.iter().find(|(q, _)| *q == seq).expect("stub names a loaded segment").1
49 }
50
51 fn slot_mut(&mut self, seq: u32) -> Option<&mut SegSlot> {
52 self.segs.iter_mut().find(|(q, _)| *q == seq).map(|(_, s)| s)
53 }
54}
55
56pub struct SealedRows {
59 pub seq: u32,
61 pub file: String,
63 keys: Vec<Vec<u8>>,
64}
65
66const ROW_TAG: &[u8] = b"rowcold:";
68
69impl ColdRef {
70 pub(crate) fn seg(seq: u32, weight: u32, _type_tag: u8) -> Self {
72 ColdRef::from_seg_parts(seq, weight)
73 }
74
75 pub(crate) fn is_seg(self) -> bool {
77 self.seg_parts().is_some()
78 }
79
80 pub(crate) fn seg_ix(self) -> u32 {
82 self.offset as u32
83 }
84}
85
86impl SegRows {
87 fn read(&self, cref: ColdRef, key: &[u8]) -> Value {
88 let slot = self.slot(cref.seg_ix());
89 let payload = slot
90 .seg
91 .get(key)
92 .expect("segrows: segment read failed — refused, not healed")
93 .unwrap_or_else(|| {
94 panic!(
95 "segrows: stub for {:?} points at segment '{}' (seq {}) which does not hold it",
96 String::from_utf8_lossy(key),
97 slot.file,
98 cref.seg_ix(),
99 )
100 });
101 tier_codec::decode(cref.type_tag, payload)
102 .expect("segrows: cold row decode failed — process bug")
103 }
104}
105
106impl Store {
107 pub fn enable_seg_rows(&mut self, dir: &Path) -> Result<(), String> {
112 if self.segrows.is_some() {
113 return Ok(());
114 }
115 let mut segs = Vec::new();
116 let mut seq = 0u32;
117 if dir.exists() {
118 let m = kevy_seg::Manifest::open(dir).map_err(|e| e.to_string())?;
119 for e in m.live().filter(|e| e.meta.starts_with(ROW_TAG)) {
120 let Some(q) = seq_of(&e.file) else {
121 return Err(format!("row segment '{}' has no parsable seq", e.file));
122 };
123 let seg = kevy_seg::Seg::open(&dir.join(&e.file))
124 .map_err(|err| format!("open {}: {err}", e.file))?;
125 seq = seq.max(q + 1);
126 segs.push((
127 q,
128 SegSlot { seg: Arc::new(seg), file: e.file.clone(), live: 0, dead: 0 },
129 ));
130 }
131 }
132 self.cold_backing |= !segs.is_empty();
136 self.segrows = Some(SegRows { dir: dir.to_path_buf(), segs, seq });
137 Ok(())
138 }
139
140 pub fn sweep_orphan_row_segs(&mut self) {
145 let Some(sr) = &mut self.segrows else { return };
146 for (_, slot) in &mut sr.segs {
147 slot.live = 0;
148 }
149 for (_, e) in &self.map {
150 if let Value::Cold(c) = &e.value
151 && c.is_seg()
152 && let Some(slot) = sr.slot_mut(c.seg_ix())
153 {
154 slot.live += 1;
155 }
156 }
157 let mut m = match kevy_seg::Manifest::open(&sr.dir) {
158 Ok(m) => m,
159 Err(_) => return,
160 };
161 sr.segs.retain(|(_, slot)| {
162 if slot.live > 0 {
163 return true;
164 }
165 let _ = m.drop_seg(&slot.file);
166 let _ = std::fs::remove_file(sr.dir.join(&slot.file));
167 false
168 });
169 let _ = m.sweep(&sr.dir);
172 }
173
174 pub fn seal_rows_to_seg(
180 &mut self,
181 table: &[u8],
182 keys: &[Vec<u8>],
183 ) -> Result<Option<SealedRows>, String> {
184 if self.segrows.is_none() {
185 return Ok(None);
186 }
187 let mut rows: Vec<(&[u8], Vec<u8>)> = Vec::with_capacity(keys.len());
188 for key in keys {
189 if let Some(payload) = self.encode_evictable_row(key) {
190 rows.push((key.as_slice(), payload));
191 }
192 }
193 if rows.is_empty() {
194 return Ok(None);
195 }
196 rows.sort_by(|a, b| a.0.cmp(b.0));
197 let seq = self.build_row_segment(table, &rows)?;
198 let file = self.segrows.as_ref().expect("enabled above").slot(seq).file.clone();
199 let sealed = rows.iter().map(|(k, _)| k.to_vec()).collect();
203 Ok(Some(SealedRows { seq, file, keys: sealed }))
204 }
205
206 pub fn commit_row_eviction(&mut self, sealed: &SealedRows) -> u64 {
209 let mut n = 0u64;
210 for key in &sealed.keys {
211 if self.demote_row_to_seg(key, sealed.seq) {
212 n += 1;
213 }
214 }
215 if let Some(sr) = self.segrows.as_mut()
216 && let Some(slot) = sr.slot_mut(sealed.seq)
217 {
218 slot.live += n;
219 }
220 n
221 }
222
223 fn encode_evictable_row(&mut self, key: &[u8]) -> Option<Vec<u8>> {
225 if self.hfttl.get(key).is_some_and(|m| !m.is_empty()) {
226 return None;
227 }
228 let e = self.live_entry(key)?;
229 if e.expire_at_ns.is_some() {
230 return None;
231 }
232 match &e.value {
233 Value::Hash(_) | Value::SmallHashInline(_) | Value::PackedRow(_) => {
234 tier_codec::encode(&e.value).map(|(payload, _tag)| payload)
235 }
236 _ => None,
237 }
238 }
239
240 fn build_row_segment(
243 &mut self,
244 table: &[u8],
245 rows: &[(&[u8], Vec<u8>)],
246 ) -> Result<u32, String> {
247 let sr = self.segrows.as_mut().expect("checked by caller");
248 std::fs::create_dir_all(&sr.dir).map_err(|e| e.to_string())?;
249 let seq = sr.seq;
250 let file = format!("row-{}-{}.seg", hex_stem(table), seq);
251 sr.seq += 1;
252 let path = sr.dir.join(&file);
253 let build = || -> Result<kevy_seg::SegMeta, String> {
254 let mut b = kevy_seg::SegBuilder::create(&path).map_err(|e| e.to_string())?;
255 for (k, payload) in rows {
256 b.push(k, payload).map_err(|e| e.to_string())?;
257 }
258 b.finish().map_err(|e| e.to_string())
259 };
260 let meta = build().inspect_err(|_| {
261 let _ = std::fs::remove_file(&path);
262 })?;
263 let mut m = kevy_seg::Manifest::open(&sr.dir).map_err(|e| e.to_string())?;
264 m.add(kevy_seg::ManifestEntry {
265 file: file.clone(),
266 meta: [ROW_TAG, table].concat(),
267 min_key: meta.min_key,
268 max_key: meta.max_key,
269 records: meta.records,
270 })
271 .map_err(|e| e.to_string())?;
272 let seg = kevy_seg::Seg::open(&path).map_err(|e| format!("reopen {file}: {e}"))?;
273 sr.segs.push((seq, SegSlot { seg: Arc::new(seg), file, live: 0, dead: 0 }));
274 self.cold_backing = true;
275 Ok(seq)
276 }
277
278 pub(crate) fn demote_row_to_seg(&mut self, key: &[u8], seg_ix: u32) -> bool {
283 let Some(e) = self.map.get_mut(key) else { return false };
284 if !matches!(e.value, Value::Hash(_) | Value::SmallHashInline(_) | Value::PackedRow(_)) {
285 return false;
286 }
287 let key_heap = key_heap_bytes_for(key);
288 let old_w = e.weight();
289 let value_w = old_w.saturating_sub(key_heap);
290 let stub = ColdRef::seg(seg_ix, value_w.min(u64::from(u32::MAX)) as u32, COLD_TAG_HASH);
291 let old_value = core::mem::replace(&mut e.value, Value::Cold(stub));
292 e.set_weight(key_heap);
293 crate::apply_delta(&mut self.used_memory, -(value_w as i64));
294 self.maybe_offload_drop(old_value);
295 true
296 }
297
298 pub(crate) fn row_seg_seq(&self, file: &str) -> Option<u32> {
300 let sr = self.segrows.as_ref()?;
301 sr.segs.iter().find(|(_, s)| s.file == file).map(|(q, _)| *q)
302 }
303
304 pub(crate) fn row_seg_records(&self, seq: u32) -> Vec<(Vec<u8>, Vec<u8>)> {
308 let sr = self.segrows.as_ref().expect("stitch ⇒ enabled");
309 let slot = sr.slot(seq);
310 let (lo, hi) = (slot.seg.meta().min_key.clone(), slot.seg.meta().max_key.clone());
311 slot.seg
312 .range(&lo, &hi)
313 .map(|r| r.expect("segrows: segment read failed — refused, not healed"))
314 .collect()
315 }
316
317 pub(crate) fn insert_row_stub(&mut self, key: &[u8], seq: u32, value_weight: u64) {
321 let stub = ColdRef::seg(seq, value_weight.min(u64::from(u32::MAX)) as u32, COLD_TAG_HASH);
322 let key_heap = key_heap_bytes_for(key);
323 let mut e = crate::Entry::new(Value::Cold(stub), None);
324 e.set_weight(key_heap);
325 crate::apply_delta(&mut self.used_memory, key_heap as i64);
326 self.map.insert(crate::SmallBytes::from_slice(key), e);
327 }
328
329 pub fn load_row_stub(&mut self, key: Vec<u8>, seq: u32, value_weight: u32) {
333 self.cold_backing = true;
334 self.insert_row_stub(&key, seq, u64::from(value_weight));
335 }
336
337 pub(crate) fn note_stitched(&mut self, seq: u32, n: u64) {
339 if let Some(sr) = self.segrows.as_mut()
340 && let Some(slot) = sr.slot_mut(seq)
341 {
342 slot.live += n;
343 }
344 }
345
346 pub(crate) fn segrow_read(&self, cref: ColdRef, key: &[u8]) -> Value {
350 self.segrows.as_ref().expect("seg-backed stub ⇒ segrows enabled").read(cref, key)
351 }
352
353 pub(crate) fn segrow_note_dead(&mut self, cref: ColdRef) {
356 if let Some(sr) = &mut self.segrows
357 && let Some(slot) = sr.slot_mut(cref.seg_ix())
358 {
359 slot.dead += 1;
360 slot.live = slot.live.saturating_sub(1);
361 }
362 }
363
364 pub fn row_seg_files(&self) -> Vec<(u32, String)> {
367 self.segrows
368 .as_ref()
369 .map(|sr| sr.segs.iter().map(|(q, s)| (*q, s.file.clone())).collect())
370 .unwrap_or_default()
371 }
372
373 pub(crate) fn segrow_pins(&self) -> Vec<(u32, Arc<kevy_seg::Seg>)> {
375 self.segrows
376 .as_ref()
377 .map(|sr| sr.segs.iter().map(|(q, s)| (*q, s.seg.clone())).collect())
378 .unwrap_or_default()
379 }
380
381 pub(crate) fn segrows_flush(&mut self) {
385 let Some(sr) = &mut self.segrows else { return };
386 if let Ok(mut m) = kevy_seg::Manifest::open(&sr.dir) {
387 let stale: Vec<String> =
388 m.live().filter(|e| e.meta.starts_with(ROW_TAG)).map(|e| e.file.clone()).collect();
389 for f in stale {
390 let _ = m.drop_seg(&f);
391 let _ = std::fs::remove_file(sr.dir.join(&f));
392 }
393 }
394 sr.segs.clear();
395 }
396}
397
398fn hex_stem(name: &[u8]) -> String {
400 name.iter().map(|b| format!("{b:02x}")).collect()
401}
402
403fn seq_of(file: &str) -> Option<u32> {
406 file.strip_suffix(".seg")?.rsplit('-').next()?.parse().ok()
407}