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((q, SegSlot { seg: Arc::new(seg), file: e.file.clone(), live: 0, dead: 0 }));
127 }
128 }
129 self.cold_backing |= !segs.is_empty();
133 self.segrows = Some(SegRows { dir: dir.to_path_buf(), segs, seq });
134 Ok(())
135 }
136
137 pub fn sweep_orphan_row_segs(&mut self) {
142 let Some(sr) = &mut self.segrows else { return };
143 for (_, slot) in &mut sr.segs {
144 slot.live = 0;
145 }
146 for (_, e) in &self.map {
147 if let Value::Cold(c) = &e.value
148 && c.is_seg()
149 && let Some(slot) = sr.slot_mut(c.seg_ix())
150 {
151 slot.live += 1;
152 }
153 }
154 let mut m = match kevy_seg::Manifest::open(&sr.dir) {
155 Ok(m) => m,
156 Err(_) => return,
157 };
158 sr.segs.retain(|(_, slot)| {
159 if slot.live > 0 {
160 return true;
161 }
162 let _ = m.drop_seg(&slot.file);
163 let _ = std::fs::remove_file(sr.dir.join(&slot.file));
164 false
165 });
166 let _ = m.sweep(&sr.dir);
169 }
170
171 pub fn seal_rows_to_seg(
177 &mut self,
178 table: &[u8],
179 keys: &[Vec<u8>],
180 ) -> Result<Option<SealedRows>, String> {
181 if self.segrows.is_none() {
182 return Ok(None);
183 }
184 let mut rows: Vec<(&[u8], Vec<u8>)> = Vec::with_capacity(keys.len());
185 for key in keys {
186 if let Some(payload) = self.encode_evictable_row(key) {
187 rows.push((key.as_slice(), payload));
188 }
189 }
190 if rows.is_empty() {
191 return Ok(None);
192 }
193 rows.sort_by(|a, b| a.0.cmp(b.0));
194 let seq = self.build_row_segment(table, &rows)?;
195 let file = self
196 .segrows
197 .as_ref()
198 .expect("enabled above")
199 .slot(seq)
200 .file
201 .clone();
202 let sealed = rows.iter().map(|(k, _)| k.to_vec()).collect();
206 Ok(Some(SealedRows { seq, file, keys: sealed }))
207 }
208
209 pub fn commit_row_eviction(&mut self, sealed: &SealedRows) -> u64 {
212 let mut n = 0u64;
213 for key in &sealed.keys {
214 if self.demote_row_to_seg(key, sealed.seq) {
215 n += 1;
216 }
217 }
218 if let Some(sr) = self.segrows.as_mut()
219 && let Some(slot) = sr.slot_mut(sealed.seq)
220 {
221 slot.live += n;
222 }
223 n
224 }
225
226 fn encode_evictable_row(&mut self, key: &[u8]) -> Option<Vec<u8>> {
228 if self.hfttl.get(key).is_some_and(|m| !m.is_empty()) {
229 return None;
230 }
231 let e = self.live_entry(key)?;
232 if e.expire_at_ns.is_some() {
233 return None;
234 }
235 match &e.value {
236 Value::Hash(_) | Value::SmallHashInline(_) => {
237 tier_codec::encode(&e.value).map(|(payload, _tag)| payload)
238 }
239 _ => None,
240 }
241 }
242
243 fn build_row_segment(&mut self, table: &[u8], rows: &[(&[u8], Vec<u8>)]) -> Result<u32, String> {
246 let sr = self.segrows.as_mut().expect("checked by caller");
247 std::fs::create_dir_all(&sr.dir).map_err(|e| e.to_string())?;
248 let seq = sr.seq;
249 let file = format!("row-{}-{}.seg", hex_stem(table), seq);
250 sr.seq += 1;
251 let path = sr.dir.join(&file);
252 let build = || -> Result<kevy_seg::SegMeta, String> {
253 let mut b = kevy_seg::SegBuilder::create(&path).map_err(|e| e.to_string())?;
254 for (k, payload) in rows {
255 b.push(k, payload).map_err(|e| e.to_string())?;
256 }
257 b.finish().map_err(|e| e.to_string())
258 };
259 let meta = build().inspect_err(|_| {
260 let _ = std::fs::remove_file(&path);
261 })?;
262 let mut m = kevy_seg::Manifest::open(&sr.dir).map_err(|e| e.to_string())?;
263 m.add(kevy_seg::ManifestEntry {
264 file: file.clone(),
265 meta: [ROW_TAG, table].concat(),
266 min_key: meta.min_key,
267 max_key: meta.max_key,
268 records: meta.records,
269 })
270 .map_err(|e| e.to_string())?;
271 let seg = kevy_seg::Seg::open(&path).map_err(|e| format!("reopen {file}: {e}"))?;
272 sr.segs.push((seq, SegSlot { seg: Arc::new(seg), file, live: 0, dead: 0 }));
273 self.cold_backing = true;
274 Ok(seq)
275 }
276
277 pub(crate) fn demote_row_to_seg(&mut self, key: &[u8], seg_ix: u32) -> bool {
282 let Some(e) = self.map.get_mut(key) else { return false };
283 if !matches!(e.value, Value::Hash(_) | Value::SmallHashInline(_)) {
284 return false;
285 }
286 let key_heap = key_heap_bytes_for(key);
287 let old_w = e.weight();
288 let value_w = old_w.saturating_sub(key_heap);
289 let stub = ColdRef::seg(seg_ix, value_w.min(u64::from(u32::MAX)) as u32, COLD_TAG_HASH);
290 let old_value = core::mem::replace(&mut e.value, Value::Cold(stub));
291 e.set_weight(key_heap);
292 crate::apply_delta(&mut self.used_memory, -(value_w as i64));
293 self.maybe_offload_drop(old_value);
294 true
295 }
296
297 pub(crate) fn row_seg_seq(&self, file: &str) -> Option<u32> {
299 let sr = self.segrows.as_ref()?;
300 sr.segs.iter().find(|(_, s)| s.file == file).map(|(q, _)| *q)
301 }
302
303 pub(crate) fn row_seg_records(&self, seq: u32) -> Vec<(Vec<u8>, Vec<u8>)> {
307 let sr = self.segrows.as_ref().expect("stitch ⇒ enabled");
308 let slot = sr.slot(seq);
309 let (lo, hi) = (slot.seg.meta().min_key.clone(), slot.seg.meta().max_key.clone());
310 slot.seg
311 .range(&lo, &hi)
312 .map(|r| r.expect("segrows: segment read failed — refused, not healed"))
313 .collect()
314 }
315
316 pub(crate) fn insert_row_stub(&mut self, key: &[u8], seq: u32, value_weight: u64) {
320 let stub = ColdRef::seg(seq, value_weight.min(u64::from(u32::MAX)) as u32, COLD_TAG_HASH);
321 let key_heap = key_heap_bytes_for(key);
322 let mut e = crate::Entry::new(Value::Cold(stub), None);
323 e.set_weight(key_heap);
324 crate::apply_delta(&mut self.used_memory, key_heap as i64);
325 self.map.insert(crate::SmallBytes::from_slice(key), e);
326 }
327
328 pub fn load_row_stub(&mut self, key: Vec<u8>, seq: u32, value_weight: u32) {
332 self.cold_backing = true;
333 self.insert_row_stub(&key, seq, u64::from(value_weight));
334 }
335
336 pub(crate) fn note_stitched(&mut self, seq: u32, n: u64) {
338 if let Some(sr) = self.segrows.as_mut()
339 && let Some(slot) = sr.slot_mut(seq)
340 {
341 slot.live += n;
342 }
343 }
344
345 pub(crate) fn segrow_read(&self, cref: ColdRef, key: &[u8]) -> Value {
349 self.segrows
350 .as_ref()
351 .expect("seg-backed stub ⇒ segrows enabled")
352 .read(cref, key)
353 }
354
355 pub(crate) fn segrow_note_dead(&mut self, cref: ColdRef) {
358 if let Some(sr) = &mut self.segrows
359 && let Some(slot) = sr.slot_mut(cref.seg_ix())
360 {
361 slot.dead += 1;
362 slot.live = slot.live.saturating_sub(1);
363 }
364 }
365
366 pub fn row_seg_files(&self) -> Vec<(u32, String)> {
369 self.segrows
370 .as_ref()
371 .map(|sr| sr.segs.iter().map(|(q, s)| (*q, s.file.clone())).collect())
372 .unwrap_or_default()
373 }
374
375 pub(crate) fn segrow_pins(&self) -> Vec<(u32, Arc<kevy_seg::Seg>)> {
377 self.segrows
378 .as_ref()
379 .map(|sr| sr.segs.iter().map(|(q, s)| (*q, s.seg.clone())).collect())
380 .unwrap_or_default()
381 }
382
383 pub(crate) fn segrows_flush(&mut self) {
387 let Some(sr) = &mut self.segrows else { return };
388 if let Ok(mut m) = kevy_seg::Manifest::open(&sr.dir) {
389 let stale: Vec<String> = m
390 .live()
391 .filter(|e| e.meta.starts_with(ROW_TAG))
392 .map(|e| e.file.clone())
393 .collect();
394 for f in stale {
395 let _ = m.drop_seg(&f);
396 let _ = std::fs::remove_file(sr.dir.join(&f));
397 }
398 }
399 sr.segs.clear();
400 }
401}
402
403fn hex_stem(name: &[u8]) -> String {
405 name.iter().map(|b| format!("{b:02x}")).collect()
406}
407
408fn seq_of(file: &str) -> Option<u32> {
411 file.strip_suffix(".seg")?.rsplit('-').next()?.parse().ok()
412}