1use std::{
5 cmp::Reverse,
6 collections::{BTreeSet, HashMap, HashSet, VecDeque},
7 iter, mem,
8 ops::Bound,
9 sync::{Arc, atomic::Ordering},
10};
11
12use reifydb_codec::key::encoded::EncodedKey;
13use reifydb_core::{
14 common::CommitVersion,
15 interface::store::EntryKind,
16 metrics::{collect::MetricsCollector, sample::MetricsSample},
17};
18use reifydb_value::{Result, byte_size::ByteSize, count::Count, util::cowvec::CowVec};
19use tracing::{Span, field, instrument};
20
21use crate::{
22 HistoricalSweep, MultiVersionScope, RangeBatch, RangeCursor, RawEntry, TierBatch, VersionedGetResult,
23 entry::{Entries, Entry, entry_bytes},
24 rows::{ActiveRows, MergedRows, Removed, RowMap, lookup, newest_across},
25};
26
27type EvictablePersist = Vec<(EncodedKey, CommitVersion, Option<CowVec<u8>>)>;
28type EvictableDrop = Vec<EvictedVersion>;
29
30const CLOSE_BYTE_THRESHOLD: ByteSize = ByteSize::from_mib(16);
31
32#[derive(Clone, Debug)]
33pub struct EvictedVersion {
34 pub key: EncodedKey,
35 pub version: CommitVersion,
36 pub value_bytes: ByteSize,
37 pub current: bool,
38}
39
40fn value_bytes_of(value: &Option<CowVec<u8>>) -> ByteSize {
41 ByteSize::from_bytes(value.as_ref().map(|v| v.len() as u64).unwrap_or(0))
42}
43
44#[derive(Clone)]
45pub struct CommitStore {
46 inner: Arc<CommitStoreInner>,
47}
48
49struct CommitStoreInner {
50 entries: Entries,
51 close_threshold: u64,
52}
53
54impl Default for CommitStore {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl CommitStore {
61 #[instrument(name = "store::multi::memory::new", level = "debug")]
62 pub fn new() -> Self {
63 Self::with_close_threshold(CLOSE_BYTE_THRESHOLD)
64 }
65
66 pub fn with_close_threshold(threshold: ByteSize) -> Self {
67 Self {
68 inner: Arc::new(CommitStoreInner {
69 entries: Entries::default(),
70 close_threshold: threshold.as_bytes().max(1),
71 }),
72 }
73 }
74
75 pub fn estimated_current_count(&self, table: EntryKind) -> Result<u64> {
76 let Some(entry) = self.inner.entries.data.get(&table) else {
77 return Ok(0);
78 };
79 Ok(entry.key_count.load(Ordering::Relaxed))
80 }
81
82 pub fn list_all_entry_kinds(&self) -> Result<Vec<EntryKind>> {
83 Ok(self.inner.entries.data.keys())
84 }
85
86 fn collect_oldest_pending(&self) -> Vec<(EntryKind, CommitVersion)> {
87 self.inner
88 .entries
89 .data
90 .keys()
91 .into_iter()
92 .filter_map(|kind| Some((kind, self.oldest_pending_for(kind)?)))
93 .collect()
94 }
95
96 pub fn list_entry_kinds_by_oldest_pending(&self) -> Result<Vec<EntryKind>> {
97 let mut pending = self.collect_oldest_pending();
98 pending.sort_by_key(|(_, version)| *version);
99 Ok(pending.into_iter().map(|(kind, _)| kind).collect())
100 }
101
102 pub fn oldest_pending_for(&self, kind: EntryKind) -> Option<CommitVersion> {
103 let entry = self.inner.entries.data.get(&kind)?;
104 let active = entry.active.read().min_version();
105 let closed = entry.closed.read().iter().map(|map| map.min_version()).min();
106 match (active, closed) {
107 (Some(a), Some(c)) => Some(a.min(c)),
108 (Some(a), None) => Some(a),
109 (None, closed) => closed,
110 }
111 }
112
113 fn resident_bytes(&self, current: bool) -> ByteSize {
114 let total = self
115 .inner
116 .entries
117 .data
118 .keys()
119 .into_iter()
120 .filter_map(|kind| self.inner.entries.data.get(&kind))
121 .map(|entry| {
122 let pick = |rows: &RowMap| {
123 if current {
124 rows.current_bytes()
125 } else {
126 rows.historical_bytes()
127 }
128 };
129 let active = pick(entry.active.read().rows());
130 let closed: u64 = entry.closed.read().iter().map(|map| pick(map.rows())).sum();
131 active + closed
132 })
133 .sum();
134 ByteSize::from_bytes(total)
135 }
136
137 pub fn current_resident_bytes(&self) -> ByteSize {
138 self.resident_bytes(true)
139 }
140
141 pub fn historical_resident_bytes(&self) -> ByteSize {
142 self.resident_bytes(false)
143 }
144
145 #[inline]
146 #[instrument(name = "store::multi::memory::get_or_create_table", level = "trace", skip(self), fields(table = ?table))]
147 fn get_or_create_table(&self, table: EntryKind) -> Arc<Entry> {
148 self.inner.entries.data.get_or_insert_with(table, || Arc::new(Entry::new()))
149 }
150
151 fn close_active(entry: &Entry, active: &mut ActiveRows) {
152 if active.is_empty() {
153 return;
154 }
155 let closing = mem::take(active);
156 entry.closed.write().push_back(Arc::new(closing.close()));
157 }
158
159 #[inline]
160 #[instrument(name = "store::multi::memory::set::table", level = "trace", skip(self, entries), fields(
161 table = ?table,
162 entry_count = entries.len(),
163 ))]
164 fn process_table(
165 &self,
166 table: EntryKind,
167 version: CommitVersion,
168 entries: Vec<(EncodedKey, Option<CowVec<u8>>)>,
169 ) {
170 let table_entry = self.get_or_create_table(table);
171 let mut active = table_entry.active_write();
172 let keys_before = active.rows().key_count();
173 let mut pending = table_entry.pending.lock();
174 for (key, value) in entries {
175 pending.insert(key.clone());
176 active.insert(key, version, value);
177 }
178 drop(pending);
179 let keys_added = active.rows().key_count() - keys_before;
180 table_entry.key_count.fetch_add(keys_added as u64, Ordering::Relaxed);
181 if active.bytes() >= self.inner.close_threshold {
182 Self::close_active(&table_entry, &mut active);
183 }
184 }
185
186 pub fn oldest_pending_version(&self) -> Option<CommitVersion> {
187 self.collect_oldest_pending().into_iter().map(|(_, version)| version).min()
188 }
189
190 fn seal_for_flush(&self, table: EntryKind, cutoff: CommitVersion) {
191 let Some(entry) = self.inner.entries.data.get(&table) else {
192 return;
193 };
194 let mut active = entry.active_write();
195 if active.min_version().is_some_and(|oldest| oldest <= cutoff) {
196 Self::close_active(&entry, &mut active);
197 }
198 }
199
200 #[instrument(name = "store::multi::memory::collect_evictable_below", level = "debug", skip_all, fields(table = ?table, cutoff = cutoff.0))]
201 pub fn collect_evictable_below(
202 &self,
203 table: EntryKind,
204 cutoff: CommitVersion,
205 budget: ByteSize,
206 ) -> (EvictablePersist, EvictableDrop, ByteSize, bool) {
207 self.seal_for_flush(table, cutoff);
208
209 let entry = match self.inner.entries.data.get(&table) {
210 Some(e) => e,
211 None => return (Vec::new(), Vec::new(), ByteSize::ZERO, false),
212 };
213 let closed = entry.closed_snapshot();
214
215 let budget = budget.as_bytes();
216 let mut consumed = 0u64;
217 let mut selected = 0usize;
218 let mut latest: HashMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)> = HashMap::new();
219 let mut to_drop: EvictableDrop = Vec::new();
220 let mut more = false;
221
222 'select: for map in closed.iter() {
223 if map.min_version() > cutoff {
224 continue;
225 }
226 for (key, versions) in map.rows().iter() {
227 if selected > 0 && consumed >= budget {
228 more = true;
229 break 'select;
230 }
231 let newest = versions.keys().next().map(|Reverse(v)| *v);
232 let mut touched = false;
233 for (Reverse(version), value) in versions.iter() {
234 if *version > cutoff {
235 continue;
236 }
237 touched = true;
238 consumed += entry_bytes(key, value);
239 to_drop.push(EvictedVersion {
240 key: key.clone(),
241 version: *version,
242 value_bytes: value_bytes_of(value),
243 current: newest == Some(*version),
244 });
245 match latest.get(key) {
246 Some((best, _)) if *best >= *version => {}
247 _ => {
248 latest.insert(key.clone(), (*version, value.clone()));
249 }
250 }
251 }
252 if touched {
253 selected += 1;
254 }
255 }
256 }
257
258 let to_persist = latest.into_iter().map(|(key, (v, val))| (key, v, val)).collect();
259 (to_persist, to_drop, ByteSize::from_bytes(consumed), more)
260 }
261}
262
263impl CommitStore {
264 #[instrument(name = "store::multi::memory::get", level = "trace", skip(self, key), fields(table = ?table, key_len = key.len(), version = version.0))]
265 pub fn get(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
266 let entry = match self.inner.entries.data.get(&table) {
267 Some(e) => e,
268 None => return Ok(VersionedGetResult::NotFound),
269 };
270
271 let active = entry.active.read();
272 let closed = entry.closed.read();
273 let found = lookup(
274 iter::once(active.rows()).chain(closed.iter().rev().map(|map| map.rows())),
275 key,
276 version,
277 );
278
279 Ok(match found {
280 Some((found, Some(value))) => VersionedGetResult::Value {
281 value: value.clone(),
282 version: found,
283 },
284 Some((_, None)) => VersionedGetResult::Tombstone,
285 None => VersionedGetResult::NotFound,
286 })
287 }
288
289 #[instrument(name = "store::multi::memory::contains", level = "trace", skip(self, key), fields(table = ?table, key_len = key.len(), version = version.0), ret)]
290 pub fn contains(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<bool> {
291 let entry = match self.inner.entries.data.get(&table) {
292 Some(e) => e,
293 None => return Ok(false),
294 };
295
296 let active = entry.active.read();
297 let closed = entry.closed.read();
298 let found = lookup(
299 iter::once(active.rows()).chain(closed.iter().rev().map(|map| map.rows())),
300 key,
301 version,
302 );
303
304 Ok(found.is_some_and(|(_, value)| value.is_some()))
305 }
306
307 #[instrument(name = "store::multi::memory::set", level = "trace", skip(self, batches), fields(
308 table_count = batches.len(),
309 total_entry_count = field::Empty,
310 version = version.0
311 ))]
312 pub fn set(&self, version: CommitVersion, batches: TierBatch) -> Result<()> {
313 let total_entries: usize = batches.values().map(|v| v.len()).sum();
314
315 batches.into_iter().for_each(|(table, entries)| {
316 self.process_table(table, version, entries);
317 });
318
319 Span::current().record("total_entry_count", total_entries);
320 Ok(())
321 }
322
323 #[instrument(name = "store::multi::memory::range_next", level = "trace", skip(self, cursor, start, end), fields(table = ?table, batch_size = batch_size, scope = ?scope))]
324 pub fn range_next(
325 &self,
326 table: EntryKind,
327 cursor: &mut RangeCursor,
328 start: Bound<&[u8]>,
329 end: Bound<&[u8]>,
330 scope: MultiVersionScope,
331 batch_size: usize,
332 ) -> Result<RangeBatch> {
333 let mut entries = Vec::with_capacity(batch_size + 1);
334 let has_more = self.range_next_into(table, cursor, start, end, scope, batch_size, &mut entries)?;
335 Ok(RangeBatch {
336 entries,
337 has_more,
338 })
339 }
340
341 #[allow(clippy::too_many_arguments)]
342 pub fn range_next_into(
343 &self,
344 table: EntryKind,
345 cursor: &mut RangeCursor,
346 start: Bound<&[u8]>,
347 end: Bound<&[u8]>,
348 scope: MultiVersionScope,
349 batch_size: usize,
350 entries: &mut Vec<RawEntry>,
351 ) -> Result<bool> {
352 entries.clear();
353
354 if cursor.is_exhausted() {
355 return Ok(false);
356 }
357
358 let entry = match self.inner.entries.data.get(&table) {
359 Some(e) => e,
360 None => {
361 cursor.finish();
362 return Ok(false);
363 }
364 };
365
366 let cursor_key = cursor.last_key().cloned();
367 let active = entry.active.read();
368 let closed = entry.closed.read();
369
370 let iter_start: Bound<&[u8]> = match &cursor_key {
371 Some(last) => Bound::Excluded(last.as_slice()),
372 None => start,
373 };
374
375 let mut merged = MergedRows::new(
376 iter::once(active.rows())
377 .chain(closed.iter().map(|map| map.rows()))
378 .map(|rows| rows.range((iter_start, end)))
379 .collect(),
380 false,
381 );
382
383 entries.reserve(batch_size + 1);
384 while entries.len() <= batch_size {
385 let Some((key, group)) = merged.next_group() else {
386 break;
387 };
388 if let Some((version, value)) = newest_across(group.iter().copied(), scope.read())
389 && scope.contains(version)
390 {
391 entries.push(RawEntry {
392 key: key.clone(),
393 version,
394 value: value.clone(),
395 });
396 }
397 }
398
399 let has_more = entries.len() > batch_size;
400 if has_more {
401 entries.truncate(batch_size);
402 }
403
404 if let Some(last_entry) = entries.last() {
405 cursor.advance(last_entry.key.clone());
406 }
407 if !has_more {
408 cursor.finish();
409 }
410
411 Ok(has_more)
412 }
413
414 #[instrument(name = "store::multi::memory::range_rev_next", level = "trace", skip(self, cursor, start, end), fields(table = ?table, batch_size = batch_size, scope = ?scope))]
415 pub fn range_rev_next(
416 &self,
417 table: EntryKind,
418 cursor: &mut RangeCursor,
419 start: Bound<&[u8]>,
420 end: Bound<&[u8]>,
421 scope: MultiVersionScope,
422 batch_size: usize,
423 ) -> Result<RangeBatch> {
424 if cursor.is_exhausted() {
425 return Ok(RangeBatch::empty());
426 }
427
428 let entry = match self.inner.entries.data.get(&table) {
429 Some(e) => e,
430 None => {
431 cursor.finish();
432 return Ok(RangeBatch::empty());
433 }
434 };
435
436 let cursor_key = cursor.last_key().cloned();
437 let active = entry.active.read();
438 let closed = entry.closed.read();
439
440 let iter_end: Bound<&[u8]> = match &cursor_key {
441 Some(last) => Bound::Excluded(last.as_slice()),
442 None => end,
443 };
444
445 let mut merged = MergedRows::new(
446 iter::once(active.rows())
447 .chain(closed.iter().map(|map| map.rows()))
448 .map(|rows| rows.range((start, iter_end)).rev())
449 .collect(),
450 true,
451 );
452
453 let mut entries: Vec<RawEntry> = Vec::with_capacity(batch_size + 1);
454 while entries.len() <= batch_size {
455 let Some((key, group)) = merged.next_group() else {
456 break;
457 };
458 if let Some((version, value)) = newest_across(group.iter().copied(), scope.read())
459 && scope.contains(version)
460 {
461 entries.push(RawEntry {
462 key: key.clone(),
463 version,
464 value: value.clone(),
465 });
466 }
467 }
468
469 let has_more = entries.len() > batch_size;
470 if has_more {
471 entries.truncate(batch_size);
472 }
473
474 if let Some(last_entry) = entries.last() {
475 cursor.advance(last_entry.key.clone());
476 }
477 if !has_more {
478 cursor.finish();
479 }
480
481 Ok(RangeBatch {
482 entries,
483 has_more,
484 })
485 }
486
487 #[instrument(name = "store::multi::memory::ensure_table", level = "trace", skip(self), fields(table = ?table))]
488 pub fn ensure_table(&self, table: EntryKind) -> Result<()> {
489 let _ = self.get_or_create_table(table);
490 Ok(())
491 }
492
493 #[instrument(name = "store::multi::memory::clear_table", level = "debug", skip(self), fields(table = ?table))]
494 pub fn clear_table(&self, table: EntryKind) -> Result<()> {
495 if let Some(entry) = self.inner.entries.data.get(&table) {
496 let mut active = entry.active_write();
497 *active = ActiveRows::new();
498 *entry.closed.write() = VecDeque::new();
499 entry.key_count.store(0, Ordering::Relaxed);
500 }
501 Ok(())
502 }
503}
504
505impl CommitStore {
506 #[instrument(name = "store::multi::memory::drop", level = "debug", skip(self, batches), fields(
507 table_count = batches.len(),
508 total_entry_count = field::Empty
509 ))]
510 pub fn compact(
511 &self,
512 batches: HashMap<EntryKind, Vec<(EncodedKey, CommitVersion)>>,
513 ) -> Result<Vec<EvictedVersion>> {
514 let total_entries: usize = batches.values().map(|v| v.len()).sum();
515 let mut removed: Vec<EvictedVersion> = Vec::with_capacity(total_entries);
516
517 for (table, entries) in batches {
518 let table_entry = self.get_or_create_table(table);
519 let mut dropped: HashMap<EncodedKey, HashSet<CommitVersion>> = HashMap::new();
520 let mut below = CommitVersion(0);
521 for (key, version) in entries {
522 below = below.max(version);
523 dropped.entry(key).or_default().insert(version);
524 }
525
526 let mut active = table_entry.active_write();
527 let mut closed = table_entry.closed.write();
528 let keys_before = active.rows().key_count()
529 + closed.iter().map(|slot| slot.rows().key_count()).sum::<usize>();
530
531 let newest: HashMap<EncodedKey, CommitVersion> = dropped
532 .keys()
533 .filter_map(|key| {
534 lookup(
535 iter::once(active.rows())
536 .chain(closed.iter().rev().map(|slot| slot.rows())),
537 key,
538 CommitVersion(u64::MAX),
539 )
540 .map(|(version, _)| (key.clone(), version))
541 })
542 .collect();
543
544 let mut record = |entry: &Removed| EvictedVersion {
545 key: entry.key.clone(),
546 version: entry.version,
547 value_bytes: value_bytes_of(&entry.value),
548 current: newest.get(&entry.key) == Some(&entry.version),
549 };
550
551 if active.min_version().is_some_and(|min| min <= below) {
552 removed.extend(active.compact(&dropped).iter().map(&mut record));
553 }
554
555 for slot in closed.iter_mut() {
556 if slot.min_version() > below {
557 continue;
558 }
559 removed.extend(Arc::make_mut(slot).compact(&dropped).iter().map(&mut record));
560 }
561 closed.retain(|map| !map.rows().is_empty());
562 let keys_after = active.rows().key_count()
563 + closed.iter().map(|slot| slot.rows().key_count()).sum::<usize>();
564 table_entry.key_count.fetch_sub((keys_before - keys_after) as u64, Ordering::Relaxed);
565 }
566
567 Span::current().record("total_entry_count", total_entries);
568 Ok(removed)
569 }
570
571 #[instrument(name = "store::multi::memory::get_all_versions", level = "trace", skip(self, key), fields(table = ?table, key_len = key.len()))]
572 pub fn get_all_versions(
573 &self,
574 table: EntryKind,
575 key: &[u8],
576 ) -> Result<Vec<(CommitVersion, Option<CowVec<u8>>)>> {
577 let entry = match self.inner.entries.data.get(&table) {
578 Some(e) => e,
579 None => return Ok(Vec::new()),
580 };
581
582 let active = entry.active.read();
583 let closed = entry.closed.read();
584
585 let mut versions: Vec<(CommitVersion, Option<CowVec<u8>>)> = iter::once(active.rows())
586 .chain(closed.iter().map(|map| map.rows()))
587 .filter_map(|rows| rows.versions_for(key))
588 .flat_map(|found| found.iter().map(|(Reverse(v), value)| (*v, value.clone())))
589 .collect();
590
591 versions.sort_by(|a, b| b.0.cmp(&a.0));
592 versions.dedup_by_key(|(version, _)| *version);
593
594 Ok(versions)
595 }
596
597 #[instrument(name = "store::multi::memory::sweep_historical_below", level = "trace", skip(self), fields(table = ?table, cutoff = cutoff.0, batch_size = batch_size))]
598 pub fn sweep_historical_below(
599 &self,
600 table: EntryKind,
601 cutoff: CommitVersion,
602 batch_size: usize,
603 ) -> Result<HistoricalSweep> {
604 let Some(entry) = self.inner.entries.data.get(&table) else {
605 return Ok(HistoricalSweep::default());
606 };
607
608 let queued = mem::take(&mut *entry.pending.lock());
609 let queued = if queued.is_empty() {
610 mem::take(&mut *entry.retained.lock())
611 } else {
612 queued
613 };
614 let mut queued = queued.into_iter();
615 let keys: Vec<EncodedKey> = queued.by_ref().take(batch_size).collect();
616 let mut unexamined: BTreeSet<EncodedKey> = queued.collect();
617 let remaining = unexamined.len() as u64;
618
619 let mut entries: Vec<(EncodedKey, CommitVersion)> = Vec::new();
620 let mut retained: Vec<EncodedKey> = Vec::new();
621 let mut versions: Vec<CommitVersion> = Vec::new();
622 for key in keys {
623 versions.clear();
624 {
625 let active = entry.active.read();
626 if let Some(found) = active.rows().versions_for(key.as_slice()) {
627 versions.extend(found.keys().map(|Reverse(version)| *version));
628 }
629 }
630 {
631 let closed = entry.closed.read();
632 for map in closed.iter() {
633 if let Some(found) = map.rows().versions_for(key.as_slice()) {
634 versions.extend(found.keys().map(|Reverse(version)| *version));
635 }
636 }
637 }
638 versions.sort_unstable();
639 versions.dedup();
640 let Some((_, older)) = versions.split_last() else {
641 continue;
642 };
643 let mut above_cutoff = false;
644 for version in older {
645 if *version < cutoff {
646 entries.push((key.clone(), *version));
647 } else {
648 above_cutoff = true;
649 }
650 }
651 if above_cutoff {
652 retained.push(key);
653 }
654 }
655
656 entry.pending.lock().append(&mut unexamined);
657 entry.retained.lock().extend(retained);
658
659 Ok(HistoricalSweep {
660 entries,
661 remaining,
662 })
663 }
664}
665
666#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
667pub struct MultiCommitMetrics {
668 pub current_bytes: ByteSize,
669 pub historical_bytes: ByteSize,
670 pub table_count: Count,
671 pub current_entries: Count,
672}
673
674impl CommitStore {
675 pub fn metrics(&self) -> MultiCommitMetrics {
676 let kinds = self.list_all_entry_kinds().unwrap_or_default();
677 let current_entries: u64 =
678 kinds.iter().map(|kind| self.estimated_current_count(*kind).unwrap_or(0)).sum();
679 MultiCommitMetrics {
680 current_bytes: self.current_resident_bytes(),
681 historical_bytes: self.historical_resident_bytes(),
682 table_count: Count::new(kinds.len() as u64),
683 current_entries: Count::new(current_entries),
684 }
685 }
686}
687
688impl MetricsCollector for CommitStore {
689 fn collect(&self, out: &mut Vec<MetricsSample>) {
690 out.push(MetricsSample::heap("commit_buffer", "current_bytes", self.current_resident_bytes()));
691 out.push(MetricsSample::heap("commit_buffer", "historical_bytes", self.historical_resident_bytes()));
692 let kinds = self.list_all_entry_kinds().unwrap_or_default();
693 out.push(MetricsSample::count("commit_buffer", "table_count", kinds.len() as u64));
694 let current_entries: u64 =
695 kinds.iter().map(|kind| self.estimated_current_count(*kind).unwrap_or(0)).sum();
696 out.push(MetricsSample::count("commit_buffer", "current_entries", current_entries));
697 }
698}
699
700#[cfg(test)]
701pub mod tests {
702 use std::collections::BTreeMap;
703
704 use reifydb_core::interface::{
705 catalog::{id::TableId, storage::StorageId},
706 store::EntryLayout,
707 };
708
709 use super::*;
710
711 const UNBOUNDED: ByteSize = ByteSize::from_bytes(u64::MAX);
712
713 fn budget_of(entries: &[(EncodedKey, Option<CowVec<u8>>)]) -> ByteSize {
714 ByteSize::from_bytes(entries.iter().map(|(key, value)| entry_bytes(key, value)).sum())
717 }
718
719 fn keyed(name: &str, value: &[u8]) -> (EncodedKey, Option<CowVec<u8>>) {
720 (EncodedKey::new(name.as_bytes().to_vec()), Some(CowVec::new(value.to_vec())))
721 }
722
723 fn seed(storage: &CommitStore, version: u64, entries: &[(EncodedKey, Option<CowVec<u8>>)]) {
724 for (key, value) in entries {
725 storage.set(
726 CommitVersion(version),
727 HashMap::from([(EntryKind::Multi, vec![(key.clone(), value.clone())])]),
728 )
729 .unwrap();
730 }
731 }
732
733 #[test]
734 fn test_basic_operations() {
735 let storage = CommitStore::new();
736
737 let key = EncodedKey::new(b"key1");
738 let version = CommitVersion(1);
739
740 storage.set(
741 version,
742 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"value1".to_vec())))])]),
743 )
744 .unwrap();
745
746 let value = storage.get(EntryKind::Multi, &key, version).unwrap().value();
747 assert_eq!(value.as_deref(), Some(b"value1".as_slice()));
748
749 assert!(storage.contains(EntryKind::Multi, &key, version).unwrap());
750
751 assert!(!storage.contains(EntryKind::Multi, b"nonexistent", version).unwrap());
752
753 let version2 = CommitVersion(2);
754 storage.set(version2, HashMap::from([(EntryKind::Multi, vec![(key.clone(), None)])])).unwrap();
755 assert!(!storage.contains(EntryKind::Multi, &key, version2).unwrap());
756 }
757
758 #[test]
759 fn test_source_tables() {
760 let storage = CommitStore::new();
761
762 let source1 = StorageId::Table(TableId(1));
763 let source2 = StorageId::Table(TableId(2));
764
765 let key = EncodedKey::new(b"key");
766 let version = CommitVersion(1);
767
768 storage.set(
769 version,
770 HashMap::from([(
771 EntryKind::Source(source1, EntryLayout::Row),
772 vec![(key.clone(), Some(CowVec::new(b"table1".to_vec())))],
773 )]),
774 )
775 .unwrap();
776 storage.set(
777 version,
778 HashMap::from([(
779 EntryKind::Source(source2, EntryLayout::Row),
780 vec![(key.clone(), Some(CowVec::new(b"table2".to_vec())))],
781 )]),
782 )
783 .unwrap();
784
785 assert_eq!(
786 storage.get(EntryKind::Source(source1, EntryLayout::Row), &key, version)
787 .unwrap()
788 .value()
789 .as_deref(),
790 Some(b"table1".as_slice())
791 );
792 assert_eq!(
793 storage.get(EntryKind::Source(source2, EntryLayout::Row), &key, version)
794 .unwrap()
795 .value()
796 .as_deref(),
797 Some(b"table2".as_slice())
798 );
799 }
800
801 #[test]
802 fn test_version_promotion_to_historical() {
803 let storage = CommitStore::new();
804
805 let key = EncodedKey::new(b"key1");
806
807 storage.set(
808 CommitVersion(1),
809 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v1".to_vec())))])]),
810 )
811 .unwrap();
812
813 storage.set(
814 CommitVersion(2),
815 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v2".to_vec())))])]),
816 )
817 .unwrap();
818
819 storage.set(
820 CommitVersion(3),
821 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v3".to_vec())))])]),
822 )
823 .unwrap();
824
825 assert_eq!(
826 storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().as_deref(),
827 Some(b"v3".as_slice())
828 );
829
830 assert_eq!(
831 storage.get(EntryKind::Multi, &key, CommitVersion(2)).unwrap().value().as_deref(),
832 Some(b"v2".as_slice())
833 );
834
835 assert_eq!(
836 storage.get(EntryKind::Multi, &key, CommitVersion(1)).unwrap().value().as_deref(),
837 Some(b"v1".as_slice())
838 );
839 }
840
841 #[test]
842 fn test_insert_older_version() {
843 let storage = CommitStore::new();
846
847 let key = EncodedKey::new(b"key1");
848
849 storage.set(
850 CommitVersion(3),
851 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v3".to_vec())))])]),
852 )
853 .unwrap();
854
855 storage.set(
856 CommitVersion(1),
857 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v1".to_vec())))])]),
858 )
859 .unwrap();
860
861 assert_eq!(
862 storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().as_deref(),
863 Some(b"v3".as_slice())
864 );
865
866 assert_eq!(
867 storage.get(EntryKind::Multi, &key, CommitVersion(1)).unwrap().value().as_deref(),
868 Some(b"v1".as_slice())
869 );
870
871 assert_eq!(
872 storage.get(EntryKind::Multi, &key, CommitVersion(2)).unwrap().value().as_deref(),
873 Some(b"v1".as_slice())
874 );
875 }
876
877 #[test]
878 fn test_range_next() {
879 let storage = CommitStore::new();
880
881 let version = CommitVersion(1);
882 storage.set(
883 version,
884 HashMap::from([(
885 EntryKind::Multi,
886 vec![
887 (EncodedKey::new(b"a"), Some(CowVec::new(b"1".to_vec()))),
888 (EncodedKey::new(b"b"), Some(CowVec::new(b"2".to_vec()))),
889 (EncodedKey::new(b"c"), Some(CowVec::new(b"3".to_vec()))),
890 ],
891 )]),
892 )
893 .unwrap();
894
895 let mut cursor = RangeCursor::new();
896 let batch = storage
897 .range_next(
898 EntryKind::Multi,
899 &mut cursor,
900 Bound::Unbounded,
901 Bound::Unbounded,
902 MultiVersionScope::AsOf {
903 read: version,
904 },
905 100,
906 )
907 .unwrap();
908
909 assert_eq!(batch.entries.len(), 3);
910 assert!(!batch.has_more);
911 assert!(cursor.is_exhausted());
912
913 assert_eq!(&*batch.entries[0].key, b"a");
914 assert_eq!(&*batch.entries[1].key, b"b");
915 assert_eq!(&*batch.entries[2].key, b"c");
916 }
917
918 #[test]
919 fn test_range_rev_next() {
920 let storage = CommitStore::new();
921
922 let version = CommitVersion(1);
923 storage.set(
924 version,
925 HashMap::from([(
926 EntryKind::Multi,
927 vec![
928 (EncodedKey::new(b"a"), Some(CowVec::new(b"1".to_vec()))),
929 (EncodedKey::new(b"b"), Some(CowVec::new(b"2".to_vec()))),
930 (EncodedKey::new(b"c"), Some(CowVec::new(b"3".to_vec()))),
931 ],
932 )]),
933 )
934 .unwrap();
935
936 let mut cursor = RangeCursor::new();
937 let batch = storage
938 .range_rev_next(
939 EntryKind::Multi,
940 &mut cursor,
941 Bound::Unbounded,
942 Bound::Unbounded,
943 MultiVersionScope::AsOf {
944 read: version,
945 },
946 100,
947 )
948 .unwrap();
949
950 assert_eq!(batch.entries.len(), 3);
951 assert!(!batch.has_more);
952 assert!(cursor.is_exhausted());
953
954 assert_eq!(&*batch.entries[0].key, b"c");
955 assert_eq!(&*batch.entries[1].key, b"b");
956 assert_eq!(&*batch.entries[2].key, b"a");
957 }
958
959 #[test]
960 fn test_range_streaming_pagination() {
961 let storage = CommitStore::new();
962
963 let version = CommitVersion(1);
964
965 let entries: Vec<_> =
966 (0..10u8).map(|i| (EncodedKey::new(vec![i]), Some(CowVec::new(vec![i * 10])))).collect();
967 storage.set(version, HashMap::from([(EntryKind::Multi, entries)])).unwrap();
968
969 let mut cursor = RangeCursor::new();
970
971 let batch1 = storage
972 .range_next(
973 EntryKind::Multi,
974 &mut cursor,
975 Bound::Unbounded,
976 Bound::Unbounded,
977 MultiVersionScope::AsOf {
978 read: version,
979 },
980 3,
981 )
982 .unwrap();
983 assert_eq!(batch1.entries.len(), 3);
984 assert!(batch1.has_more);
985 assert!(!cursor.is_exhausted());
986
987 assert_eq!(&*batch1.entries[0].key, &[0]);
988 assert_eq!(&*batch1.entries[2].key, &[2]);
989
990 let batch2 = storage
991 .range_next(
992 EntryKind::Multi,
993 &mut cursor,
994 Bound::Unbounded,
995 Bound::Unbounded,
996 MultiVersionScope::AsOf {
997 read: version,
998 },
999 3,
1000 )
1001 .unwrap();
1002 assert_eq!(batch2.entries.len(), 3);
1003 assert!(batch2.has_more);
1004 assert!(!cursor.is_exhausted());
1005
1006 assert_eq!(&*batch2.entries[0].key, &[3]);
1007 assert_eq!(&*batch2.entries[2].key, &[5]);
1008
1009 let batch3 = storage
1010 .range_next(
1011 EntryKind::Multi,
1012 &mut cursor,
1013 Bound::Unbounded,
1014 Bound::Unbounded,
1015 MultiVersionScope::AsOf {
1016 read: version,
1017 },
1018 3,
1019 )
1020 .unwrap();
1021 assert_eq!(batch3.entries.len(), 3);
1022 assert!(batch3.has_more);
1023 assert!(!cursor.is_exhausted());
1024
1025 assert_eq!(&*batch3.entries[0].key, &[6]);
1026 assert_eq!(&*batch3.entries[2].key, &[8]);
1027
1028 let batch4 = storage
1029 .range_next(
1030 EntryKind::Multi,
1031 &mut cursor,
1032 Bound::Unbounded,
1033 Bound::Unbounded,
1034 MultiVersionScope::AsOf {
1035 read: version,
1036 },
1037 3,
1038 )
1039 .unwrap();
1040 assert_eq!(batch4.entries.len(), 1);
1041 assert!(!batch4.has_more);
1042 assert!(cursor.is_exhausted());
1043
1044 assert_eq!(&*batch4.entries[0].key, &[9]);
1045
1046 let batch5 = storage
1047 .range_next(
1048 EntryKind::Multi,
1049 &mut cursor,
1050 Bound::Unbounded,
1051 Bound::Unbounded,
1052 MultiVersionScope::AsOf {
1053 read: version,
1054 },
1055 3,
1056 )
1057 .unwrap();
1058 assert!(batch5.entries.is_empty());
1059 }
1060
1061 #[test]
1062 fn test_range_reving_pagination() {
1063 let storage = CommitStore::new();
1064
1065 let version = CommitVersion(1);
1066
1067 let entries: Vec<_> =
1068 (0..10u8).map(|i| (EncodedKey::new(vec![i]), Some(CowVec::new(vec![i * 10])))).collect();
1069 storage.set(version, HashMap::from([(EntryKind::Multi, entries)])).unwrap();
1070
1071 let mut cursor = RangeCursor::new();
1072
1073 let batch1 = storage
1074 .range_rev_next(
1075 EntryKind::Multi,
1076 &mut cursor,
1077 Bound::Unbounded,
1078 Bound::Unbounded,
1079 MultiVersionScope::AsOf {
1080 read: version,
1081 },
1082 3,
1083 )
1084 .unwrap();
1085 assert_eq!(batch1.entries.len(), 3);
1086 assert!(batch1.has_more);
1087 assert!(!cursor.is_exhausted());
1088
1089 assert_eq!(&*batch1.entries[0].key, &[9]);
1090 assert_eq!(&*batch1.entries[2].key, &[7]);
1091
1092 let batch2 = storage
1093 .range_rev_next(
1094 EntryKind::Multi,
1095 &mut cursor,
1096 Bound::Unbounded,
1097 Bound::Unbounded,
1098 MultiVersionScope::AsOf {
1099 read: version,
1100 },
1101 3,
1102 )
1103 .unwrap();
1104 assert_eq!(batch2.entries.len(), 3);
1105 assert!(batch2.has_more);
1106 assert!(!cursor.is_exhausted());
1107
1108 assert_eq!(&*batch2.entries[0].key, &[6]);
1109 assert_eq!(&*batch2.entries[2].key, &[4]);
1110 }
1111
1112 fn sweep_all(storage: &CommitStore, cutoff: u64, batch: usize) -> Vec<(EncodedKey, CommitVersion)> {
1113 let mut all = Vec::new();
1116 let mut calls = 0;
1117 loop {
1118 let sweep =
1119 storage.sweep_historical_below(EntryKind::Multi, CommitVersion(cutoff), batch).unwrap();
1120 calls += 1;
1121 assert!(calls <= 64, "a pass must terminate even when every key is retained");
1122 all.extend(sweep.entries.iter().cloned());
1123 if !sweep.entries.is_empty() {
1124 storage.compact(HashMap::from([(EntryKind::Multi, sweep.entries)])).unwrap();
1125 }
1126 if sweep.remaining == 0 {
1127 return all;
1128 }
1129 }
1130 }
1131
1132 #[test]
1133 fn sweep_drops_versions_below_cutoff_across_closed_maps_and_forgets_finished_keys() {
1134 let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
1139 for (name, version) in [
1140 ("a", 1),
1141 ("a", 2),
1142 ("a", 3),
1143 ("b", 1),
1144 ("b", 2),
1145 ("c", 1),
1146 ("d", 1),
1147 ("d", 2),
1148 ("d", 4),
1149 ("e", 3),
1150 ("e", 2),
1151 ("e", 1),
1152 ] {
1153 seed(&storage, version, &[keyed(name, b"v")]);
1154 }
1155 let row =
1156 |name: &str, version: u64| (EncodedKey::new(name.as_bytes().to_vec()), CommitVersion(version));
1157 let expected =
1158 vec![row("a", 1), row("a", 2), row("b", 1), row("d", 1), row("d", 2), row("e", 1), row("e", 2)];
1159
1160 assert_eq!(sweep_all(&storage, 4, 100), expected);
1161 for (name, survivors) in
1162 [("a", vec![3]), ("b", vec![2]), ("c", vec![1]), ("d", vec![4]), ("e", vec![3])]
1163 {
1164 let left: Vec<u64> = storage
1165 .get_all_versions(EntryKind::Multi, name.as_bytes())
1166 .unwrap()
1167 .into_iter()
1168 .map(|(v, _)| v.0)
1169 .collect();
1170 assert_eq!(left, survivors, "key {name}");
1171 }
1172
1173 let again = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(100), 100).unwrap();
1174 assert!(again.entries.is_empty());
1175 assert_eq!(again.remaining, 0);
1176 }
1177
1178 #[test]
1179 fn sweep_requeues_keys_whose_older_versions_are_still_above_cutoff() {
1180 let storage = CommitStore::new();
1184 let key = EncodedKey::new(b"k".to_vec());
1185 for version in [1, 2, 3] {
1186 seed(&storage, version, &[keyed("k", b"v")]);
1187 }
1188
1189 let first = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(2), 100).unwrap();
1190 assert_eq!(first.entries, vec![(key.clone(), CommitVersion(1))]);
1191 assert_eq!(first.remaining, 0, "a retained key is not backlog");
1192 storage.compact(HashMap::from([(EntryKind::Multi, first.entries)])).unwrap();
1193
1194 let second = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(3), 100).unwrap();
1195 assert_eq!(
1196 second.entries,
1197 vec![(key.clone(), CommitVersion(2))],
1198 "requeued key reclaimed once the cutoff passed"
1199 );
1200 storage.compact(HashMap::from([(EntryKind::Multi, second.entries)])).unwrap();
1201
1202 let third = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(100), 100).unwrap();
1203 assert!(third.entries.is_empty(), "nothing historical is left for the key");
1204 }
1205
1206 #[test]
1207 fn a_retained_key_waits_for_the_next_pass_instead_of_displacing_unexamined_ones() {
1208 let storage = CommitStore::new();
1212 let names = ["a", "b", "c", "d", "e"];
1213 for name in names {
1214 seed(&storage, 1, &[keyed(name, b"v1")]);
1215 seed(&storage, 2, &[keyed(name, b"v2")]);
1216 seed(&storage, 3, &[keyed(name, b"v3")]);
1217 }
1218
1219 let dropped = |sweep: &HistoricalSweep| -> Vec<(String, u64)> {
1220 sweep.entries
1221 .iter()
1222 .map(|(key, version)| (String::from_utf8(key.as_slice().to_vec()).unwrap(), version.0))
1223 .collect()
1224 };
1225
1226 let first = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(2), 2).unwrap();
1227 assert_eq!(dropped(&first), vec![("a".into(), 1), ("b".into(), 1)]);
1228 assert_eq!(first.remaining, 3);
1229 storage.compact(HashMap::from([(EntryKind::Multi, first.entries)])).unwrap();
1230
1231 let second = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(2), 2).unwrap();
1232 assert_eq!(
1233 dropped(&second),
1234 vec![("c".into(), 1), ("d".into(), 1)],
1235 "a and b were retained, not re-batched"
1236 );
1237 assert_eq!(second.remaining, 1);
1238 storage.compact(HashMap::from([(EntryKind::Multi, second.entries)])).unwrap();
1239
1240 let third = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(2), 2).unwrap();
1241 assert_eq!(dropped(&third), vec![("e".into(), 1)]);
1242 assert_eq!(third.remaining, 0, "the pass ends once every fresh key was examined");
1243 storage.compact(HashMap::from([(EntryKind::Multi, third.entries)])).unwrap();
1244
1245 let next_pass = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(3), 2).unwrap();
1246 assert_eq!(
1247 dropped(&next_pass),
1248 vec![("a".into(), 2), ("b".into(), 2)],
1249 "retained keys come back once the queue is empty"
1250 );
1251 assert_eq!(next_pass.remaining, 3, "the rest of the retained keys are the new backlog");
1252 }
1253
1254 #[test]
1255 fn sweep_examines_at_most_batch_keys_and_reports_the_rest_as_backlog() {
1256 let storage = CommitStore::new();
1260 let names = ["k1", "k2", "k3", "k4", "k5"];
1261 for name in names {
1262 seed(&storage, 1, &[keyed(name, b"old")]);
1263 seed(&storage, 2, &[keyed(name, b"new")]);
1264 }
1265 seed(&storage, 2, &[keyed("z-single", b"single")]);
1266
1267 let first = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(10), 2).unwrap();
1268 assert_eq!(first.entries.len(), 2);
1269 assert_eq!(first.remaining, 4, "six keys queued, two examined");
1270 storage.compact(HashMap::from([(EntryKind::Multi, first.entries.clone())])).unwrap();
1271
1272 let mut all = first.entries;
1273 all.extend(sweep_all(&storage, 10, 2));
1274 let mut expected: Vec<(EncodedKey, CommitVersion)> = names
1275 .iter()
1276 .map(|name| (EncodedKey::new(name.as_bytes().to_vec()), CommitVersion(1)))
1277 .collect();
1278 expected.sort();
1279 all.sort();
1280 assert_eq!(all, expected);
1281
1282 let after = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(10), 100).unwrap();
1283 assert!(after.entries.is_empty());
1284 assert_eq!(after.remaining, 0, "a single-version key does not linger in the queue");
1285 }
1286
1287 #[test]
1288 fn test_drop_from_historical() {
1289 let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
1290
1291 let key = EncodedKey::new(b"key1");
1292
1293 for v in 1..=3u64 {
1294 storage.set(
1295 CommitVersion(v),
1296 HashMap::from([(
1297 EntryKind::Multi,
1298 vec![(key.clone(), Some(CowVec::new(format!("v{}", v).into_bytes())))],
1299 )]),
1300 )
1301 .unwrap();
1302 }
1303
1304 storage.compact(HashMap::from([(EntryKind::Multi, vec![(key.clone(), CommitVersion(1))])])).unwrap();
1305
1306 assert!(storage.get(EntryKind::Multi, &key, CommitVersion(1)).unwrap().value().is_none());
1307
1308 assert_eq!(
1309 storage.get(EntryKind::Multi, &key, CommitVersion(2)).unwrap().value().as_deref(),
1310 Some(b"v2".as_slice())
1311 );
1312 assert_eq!(
1313 storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().as_deref(),
1314 Some(b"v3".as_slice())
1315 );
1316 }
1317 #[test]
1318 fn compact_returns_each_removed_historical_version_flagged_as_not_current() {
1319 let storage = CommitStore::new();
1322
1323 let key = EncodedKey::new(b"key1");
1324
1325 for v in 1..=3u64 {
1326 storage.set(
1327 CommitVersion(v),
1328 HashMap::from([(
1329 EntryKind::Multi,
1330 vec![(key.clone(), Some(CowVec::new(format!("v{}", v).into_bytes())))],
1331 )]),
1332 )
1333 .unwrap();
1334 }
1335 storage.seal_for_flush(EntryKind::Multi, CommitVersion(u64::MAX));
1336
1337 let removed = storage
1338 .compact(HashMap::from([(
1339 EntryKind::Multi,
1340 vec![(key.clone(), CommitVersion(1)), (key.clone(), CommitVersion(2))],
1341 )]))
1342 .unwrap();
1343
1344 let mut versions: Vec<u64> = removed.iter().map(|entry| entry.version.0).collect();
1345 versions.sort_unstable();
1346 assert_eq!(versions, vec![1, 2]);
1347 assert!(removed.iter().all(|entry| !entry.current));
1348 assert!(removed.iter().all(|entry| entry.value_bytes == ByteSize::from_bytes(2)));
1349 }
1350
1351 #[test]
1352 fn compact_reports_the_live_version_and_leaves_surviving_history_in_place() {
1353 let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
1357
1358 let key = EncodedKey::new(b"key1");
1359
1360 for v in 1..=3u64 {
1361 storage.set(
1362 CommitVersion(v),
1363 HashMap::from([(
1364 EntryKind::Multi,
1365 vec![(key.clone(), Some(CowVec::new(format!("v{}", v).into_bytes())))],
1366 )]),
1367 )
1368 .unwrap();
1369 }
1370
1371 let removed = storage
1372 .compact(HashMap::from([(EntryKind::Multi, vec![(key.clone(), CommitVersion(3))])]))
1373 .unwrap();
1374
1375 assert_eq!(removed.len(), 1, "only the live version was dropped");
1376 assert_eq!(removed[0].version, CommitVersion(3));
1377 assert!(removed[0].current, "the dropped version was the live one");
1378 assert_eq!(removed[0].value_bytes, ByteSize::from_bytes(2));
1379
1380 assert_eq!(
1381 storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().as_deref(),
1382 Some(b"v2".as_slice()),
1383 "the newest survivor is still readable from historical without being promoted"
1384 );
1385 }
1386
1387 #[test]
1388 fn compact_reports_the_live_entry_when_every_version_of_a_key_is_removed() {
1389 let storage = CommitStore::new();
1393
1394 let key = EncodedKey::new(b"key1");
1395
1396 for v in 1..=3u64 {
1397 storage.set(
1398 CommitVersion(v),
1399 HashMap::from([(
1400 EntryKind::Multi,
1401 vec![(key.clone(), Some(CowVec::new(format!("v{}", v).into_bytes())))],
1402 )]),
1403 )
1404 .unwrap();
1405 }
1406 storage.seal_for_flush(EntryKind::Multi, CommitVersion(u64::MAX));
1407
1408 let removed = storage
1409 .compact(HashMap::from([(
1410 EntryKind::Multi,
1411 vec![
1412 (key.clone(), CommitVersion(1)),
1413 (key.clone(), CommitVersion(2)),
1414 (key.clone(), CommitVersion(3)),
1415 ],
1416 )]))
1417 .unwrap();
1418
1419 assert_eq!(removed.len(), 3);
1420
1421 let live: Vec<u64> =
1422 removed.iter().filter(|entry| entry.current).map(|entry| entry.version.0).collect();
1423 assert_eq!(live, vec![3]);
1424
1425 let mut historical: Vec<u64> =
1426 removed.iter().filter(|entry| !entry.current).map(|entry| entry.version.0).collect();
1427 historical.sort_unstable();
1428 assert_eq!(historical, vec![1, 2]);
1429 }
1430
1431 #[test]
1432 fn test_tombstones() {
1433 let storage = CommitStore::new();
1434
1435 let key = EncodedKey::new(b"key1");
1436
1437 storage.set(
1438 CommitVersion(1),
1439 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"value".to_vec())))])]),
1440 )
1441 .unwrap();
1442
1443 storage.set(CommitVersion(2), HashMap::from([(EntryKind::Multi, vec![(key.clone(), None)])])).unwrap();
1444
1445 assert!(storage.get(EntryKind::Multi, &key, CommitVersion(2)).unwrap().value().is_none());
1446 assert!(!storage.contains(EntryKind::Multi, &key, CommitVersion(2)).unwrap());
1447
1448 assert_eq!(
1449 storage.get(EntryKind::Multi, &key, CommitVersion(1)).unwrap().value().as_deref(),
1450 Some(b"value".as_slice())
1451 );
1452 }
1453
1454 #[test]
1455 fn test_collect_evictable_below_keeps_versions_above_cutoff() {
1456 let storage = CommitStore::new();
1457 let key = EncodedKey::new(b"k");
1458 for v in 1..=3u64 {
1459 storage.set(
1460 CommitVersion(v),
1461 HashMap::from([(
1462 EntryKind::Multi,
1463 vec![(key.clone(), Some(CowVec::new(format!("v{v}").into_bytes())))],
1464 )]),
1465 )
1466 .unwrap();
1467 }
1468
1469 let (to_persist, to_drop, _consumed, _more) =
1471 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(2), UNBOUNDED);
1472 assert_eq!(to_persist.len(), 1);
1473 assert_eq!(to_persist[0].0, key);
1474 assert_eq!(to_persist[0].1, CommitVersion(2));
1475 assert_eq!(to_persist[0].2.as_deref(), Some(b"v2".as_slice()));
1476 let dropped: HashSet<CommitVersion> = to_drop.iter().map(|e| e.version).collect();
1477 assert_eq!(dropped, HashSet::from([CommitVersion(1), CommitVersion(2)]));
1478
1479 storage.compact(HashMap::from([(
1480 EntryKind::Multi,
1481 to_drop.into_iter().map(|e| (e.key, e.version)).collect(),
1482 )]))
1483 .unwrap();
1484 assert_eq!(
1485 storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().as_deref(),
1486 Some(b"v3".as_slice())
1487 );
1488 assert!(storage.get(EntryKind::Multi, &key, CommitVersion(2)).unwrap().value().is_none());
1489 assert!(storage.get(EntryKind::Multi, &key, CommitVersion(1)).unwrap().value().is_none());
1490 }
1491
1492 #[test]
1493 fn test_collect_evictable_below_empty_when_all_above_cutoff() {
1494 let storage = CommitStore::new();
1495 let key = EncodedKey::new(b"k");
1496 storage.set(
1497 CommitVersion(5),
1498 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v".to_vec())))])]),
1499 )
1500 .unwrap();
1501 let (to_persist, to_drop, _consumed, _more) =
1502 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(3), UNBOUNDED);
1503 assert!(to_persist.is_empty());
1504 assert!(to_drop.is_empty());
1505 }
1506
1507 #[test]
1508 fn test_collect_evictable_below_persists_exactly_one_value_per_key() {
1509 let storage = CommitStore::new();
1512 let key = EncodedKey::new(b"k");
1513 for v in 1..=5u64 {
1514 storage.set(
1515 CommitVersion(v),
1516 HashMap::from([(
1517 EntryKind::Multi,
1518 vec![(key.clone(), Some(CowVec::new(format!("v{v}").into_bytes())))],
1519 )]),
1520 )
1521 .unwrap();
1522 }
1523
1524 let (to_persist, to_drop, _consumed, _more) =
1525 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(4), UNBOUNDED);
1526 assert_eq!(to_persist.len(), 1, "exactly one value persisted per key");
1527 assert_eq!(to_persist[0].1, CommitVersion(4), "the latest version <= cutoff");
1528 assert_eq!(to_persist[0].2.as_deref(), Some(b"v4".as_slice()));
1529
1530 let dropped: HashSet<CommitVersion> = to_drop.iter().map(|e| e.version).collect();
1531 assert_eq!(
1532 dropped,
1533 HashSet::from([CommitVersion(1), CommitVersion(2), CommitVersion(3), CommitVersion(4)])
1534 );
1535 }
1536
1537 #[test]
1538 fn test_collect_evictable_below_persists_tombstone_when_it_is_the_latest() {
1539 let storage = CommitStore::new();
1542 let key = EncodedKey::new(b"k");
1543 storage.set(
1544 CommitVersion(1),
1545 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v1".to_vec())))])]),
1546 )
1547 .unwrap();
1548 storage.set(CommitVersion(2), HashMap::from([(EntryKind::Multi, vec![(key.clone(), None)])])).unwrap();
1549
1550 let (to_persist, to_drop, _consumed, _more) =
1551 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(2), UNBOUNDED);
1552 assert_eq!(to_persist.len(), 1);
1553 assert_eq!(to_persist[0].1, CommitVersion(2), "the tombstone is the latest version");
1554 assert!(to_persist[0].2.is_none(), "the persisted latest value must be the tombstone, not v1");
1555 assert_eq!(to_drop.len(), 2, "both v1 and the tombstone are dropped from the buffer");
1556 }
1557
1558 #[test]
1559 fn test_collect_evictable_below_only_drops_historical_when_current_is_above_cutoff() {
1560 let storage = CommitStore::new();
1563 let key = EncodedKey::new(b"k");
1564 storage.set(
1565 CommitVersion(2),
1566 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v2".to_vec())))])]),
1567 )
1568 .unwrap();
1569 storage.set(
1570 CommitVersion(5),
1571 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v5".to_vec())))])]),
1572 )
1573 .unwrap();
1574
1575 let (to_persist, to_drop, _consumed, _more) =
1576 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(3), UNBOUNDED);
1577 assert_eq!(to_persist.len(), 1);
1578 assert_eq!(to_persist[0].1, CommitVersion(2), "only the aged-out historical version is persisted");
1579 assert_eq!(to_persist[0].2.as_deref(), Some(b"v2".as_slice()));
1580 let dropped: HashSet<CommitVersion> = to_drop.iter().map(|e| e.version).collect();
1581 assert_eq!(dropped, HashSet::from([CommitVersion(2)]), "v5 (current, > cutoff) is never dropped");
1582
1583 storage.compact(HashMap::from([(
1584 EntryKind::Multi,
1585 to_drop.into_iter().map(|e| (e.key, e.version)).collect(),
1586 )]))
1587 .unwrap();
1588 assert_eq!(
1589 storage.get(EntryKind::Multi, &key, CommitVersion(5)).unwrap().value().as_deref(),
1590 Some(b"v5".as_slice())
1591 );
1592 assert!(
1593 storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().is_none(),
1594 "the v2 a reader at snapshot 3 used to see is gone from the buffer after eviction"
1595 );
1596 }
1597
1598 #[test]
1599 fn test_collect_evictable_below_handles_multiple_keys_independently() {
1600 let storage = CommitStore::new();
1603 let cold = EncodedKey::new(b"cold");
1604 let hot = EncodedKey::new(b"hot");
1605 storage.set(
1606 CommitVersion(1),
1607 HashMap::from([(EntryKind::Multi, vec![(cold.clone(), Some(CowVec::new(b"cold1".to_vec())))])]),
1608 )
1609 .unwrap();
1610 storage.set(
1611 CommitVersion(9),
1612 HashMap::from([(EntryKind::Multi, vec![(hot.clone(), Some(CowVec::new(b"hot9".to_vec())))])]),
1613 )
1614 .unwrap();
1615
1616 let (to_persist, to_drop, _consumed, _more) =
1617 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(5), UNBOUNDED);
1618 assert_eq!(to_persist.len(), 1, "only the cold key is evictable below the cutoff");
1619 assert_eq!(to_persist[0].0, cold);
1620 assert!(to_drop.iter().all(|e| e.key == cold), "the hot key must not be scheduled for drop");
1621 }
1622
1623 #[test]
1624 fn test_collect_evictable_below_bounds_to_budget_and_drains_across_calls() {
1625 let storage = CommitStore::new();
1628 let entries: Vec<(EncodedKey, Option<CowVec<u8>>)> =
1629 (0..5u8).map(|i| keyed(&format!("k{i}"), &[i])).collect();
1630 seed(&storage, 1, &entries);
1631 let budget = budget_of(&entries[..2]);
1632
1633 let (to_persist, to_drop, consumed, more) =
1634 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), budget);
1635 assert_eq!(to_persist.len(), 2, "the budget only covers two entries' worth of bytes");
1636 assert_eq!(to_drop.len(), 2);
1637 assert_eq!(consumed, budget, "the reported spend must be the bytes actually selected");
1638 assert!(more, "three keys remain below the cutoff");
1639
1640 let mut drained = to_persist.len();
1641 let mut compaction_batch: HashMap<EntryKind, Vec<(EncodedKey, CommitVersion)>> = HashMap::new();
1642 compaction_batch.insert(EntryKind::Multi, to_drop.into_iter().map(|e| (e.key, e.version)).collect());
1643 storage.compact(compaction_batch).unwrap();
1644 loop {
1645 let (p, d, _consumed, more) =
1646 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), budget);
1647 if p.is_empty() {
1648 assert!(!more, "an empty collect must not claim more remains");
1649 break;
1650 }
1651 drained += p.len();
1652 let mut batch: HashMap<EntryKind, Vec<(EncodedKey, CommitVersion)>> = HashMap::new();
1653 batch.insert(EntryKind::Multi, d.into_iter().map(|e| (e.key, e.version)).collect());
1654 storage.compact(batch).unwrap();
1655 if !more {
1656 break;
1657 }
1658 }
1659 assert_eq!(drained, 5, "every below-cutoff key is drained exactly once");
1660 }
1661
1662 #[test]
1663 fn collect_evictable_below_bounds_the_slice_by_bytes_not_by_key_count() {
1664 let thin: Vec<(EncodedKey, Option<CowVec<u8>>)> =
1668 (0..8u8).map(|i| keyed(&format!("k{i}"), &[i])).collect();
1669 let fat: Vec<(EncodedKey, Option<CowVec<u8>>)> =
1670 (0..8u8).map(|i| keyed(&format!("k{i}"), &vec![i; 4096])).collect();
1671
1672 let budget = budget_of(&thin[..4]);
1673
1674 let thin_storage = CommitStore::new();
1675 seed(&thin_storage, 1, &thin);
1676 let (thin_persist, _, _, thin_more) =
1677 thin_storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), budget);
1678
1679 let fat_storage = CommitStore::new();
1680 seed(&fat_storage, 1, &fat);
1681 let (fat_persist, _, fat_consumed, fat_more) =
1682 fat_storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), budget);
1683
1684 assert_eq!(thin_persist.len(), 4, "four thin entries is exactly what the budget buys");
1685 assert!(thin_more, "four of the eight thin keys are still pending");
1686 assert_eq!(fat_persist.len(), 1, "a single fat entry already exceeds the same byte budget");
1687 assert!(fat_more, "the remaining fat keys are still pending");
1688 assert!(
1689 fat_consumed.as_bytes() > budget.as_bytes(),
1690 "the one admitted fat entry is what pushed the slice over the budget"
1691 );
1692 }
1693
1694 #[test]
1695 fn collect_evictable_below_always_admits_one_entry_even_when_the_budget_cannot_cover_it() {
1696 let storage = CommitStore::new();
1700 let entries = vec![keyed("huge-a", &vec![7u8; 65536]), keyed("huge-b", &vec![9u8; 65536])];
1701 seed(&storage, 1, &entries);
1702
1703 let (to_persist, to_drop, consumed, more) =
1704 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), ByteSize::from_bytes(1));
1705 assert_eq!(to_persist.len(), 1, "the oversized entry must be admitted, not skipped");
1706 assert_eq!(to_drop.len(), 1);
1707 assert!(consumed.as_bytes() > 65536, "the whole oversized entry counts against the slice");
1708 assert!(more, "the second oversized entry is still pending");
1709
1710 let (to_persist, _, consumed, more) =
1711 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), ByteSize::ZERO);
1712 assert_eq!(to_persist.len(), 1, "even a zero budget must still release exactly one entry");
1713 assert!(consumed.as_bytes() > 65536);
1714 assert!(more, "a zero budget yields after the one entry it was forced to take");
1715 }
1716
1717 #[test]
1718 fn collect_evictable_below_counts_every_evicted_version_of_a_key_against_the_budget() {
1719 let storage = CommitStore::new();
1723 let key = EncodedKey::new(b"k".to_vec());
1724 for v in 1..=4u64 {
1725 storage.set(
1726 CommitVersion(v),
1727 HashMap::from([(
1728 EntryKind::Multi,
1729 vec![(key.clone(), Some(CowVec::new(vec![v as u8; 512])))],
1730 )]),
1731 )
1732 .unwrap();
1733 }
1734
1735 let (_, to_drop, consumed, _) =
1736 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(4), UNBOUNDED);
1737
1738 assert_eq!(to_drop.len(), 4, "all four versions leave the buffer");
1739 let expected: u64 = (1..=4u64).map(|v| entry_bytes(&key, &Some(CowVec::new(vec![v as u8; 512])))).sum();
1740 assert_eq!(
1741 consumed,
1742 ByteSize::from_bytes(expected),
1743 "the slice must charge itself for every version it evicted, not just the live one"
1744 );
1745 }
1746
1747 fn walk_versions(storage: &CommitStore, table: EntryKind) -> BTreeMap<EncodedKey, Vec<CommitVersion>> {
1748 let entry = storage.inner.entries.data.get(&table).expect("table exists");
1751 let active = entry.active.read();
1752 let closed = entry.closed_snapshot();
1753 let mut walked: BTreeMap<EncodedKey, Vec<CommitVersion>> = BTreeMap::new();
1754 for rows in iter::once(active.rows()).chain(closed.iter().map(|map| map.rows())) {
1755 for (key, versions) in rows.iter() {
1756 walked.entry(key.clone())
1757 .or_default()
1758 .extend(versions.keys().map(|Reverse(version)| *version));
1759 }
1760 }
1761 walked
1762 }
1763
1764 fn oldest_of(storage: &CommitStore, table: EntryKind, key: &EncodedKey) -> Option<CommitVersion> {
1765 walk_versions(storage, table).get(key).and_then(|versions| versions.iter().min().copied())
1766 }
1767
1768 fn assert_oldest_pending_tracks_the_maps(storage: &CommitStore, table: EntryKind) {
1769 let walked = walk_versions(storage, table);
1772 let smallest = walked.values().flat_map(|versions| versions.iter().copied()).min();
1773 assert_eq!(
1774 storage.oldest_pending_for(table),
1775 smallest,
1776 "the reported oldest pending version must equal the smallest version the maps still hold"
1777 );
1778
1779 for (key, versions) in walked.iter() {
1780 let reported = storage
1781 .get_all_versions(table, key.as_ref())
1782 .unwrap()
1783 .last()
1784 .map(|(version, _)| *version);
1785 assert_eq!(
1786 reported,
1787 versions.iter().min().copied(),
1788 "a resident key must report its smallest stored version, not a newer one"
1789 );
1790 }
1791 }
1792
1793 #[test]
1794 fn index_stays_consistent_across_new_monotonic_out_of_order_and_drops() {
1795 let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
1799 let kind = EntryKind::Multi;
1800 let a = EncodedKey::new(b"a");
1801 let b = EncodedKey::new(b"b");
1802 let c = EncodedKey::new(b"c");
1803
1804 let set = |v: u64, key: &EncodedKey, val: &str| {
1805 storage.set(
1806 CommitVersion(v),
1807 HashMap::from([(
1808 kind,
1809 vec![(key.clone(), Some(CowVec::new(val.as_bytes().to_vec())))],
1810 )]),
1811 )
1812 .unwrap();
1813 };
1814
1815 set(10, &a, "a10");
1816 set(20, &a, "a20");
1817 set(3, &a, "a3");
1818 set(5, &b, "b5");
1819 set(7, &c, "c7");
1820 assert_eq!(
1821 oldest_of(&storage, kind, &a),
1822 Some(CommitVersion(3)),
1823 "an out-of-order write below the current version must lower a's bucket to 3"
1824 );
1825 assert_oldest_pending_tracks_the_maps(&storage, kind);
1826
1827 storage.compact(HashMap::from([(kind, vec![(a.clone(), CommitVersion(3))])])).unwrap();
1828 assert_eq!(
1829 oldest_of(&storage, kind, &a),
1830 Some(CommitVersion(10)),
1831 "dropping the oldest version must raise the bucket to the next-smallest stored version"
1832 );
1833 assert_oldest_pending_tracks_the_maps(&storage, kind);
1834
1835 storage.compact(HashMap::from([(kind, vec![(b.clone(), CommitVersion(5))])])).unwrap();
1836 assert_eq!(oldest_of(&storage, kind, &b), None, "a fully dropped key must leave the index entirely");
1837 assert_oldest_pending_tracks_the_maps(&storage, kind);
1838 }
1839
1840 #[test]
1841 fn out_of_order_landing_is_selected_for_eviction() {
1842 let storage = CommitStore::new();
1845 let key = EncodedKey::new(b"k");
1846 storage.set(
1847 CommitVersion(20),
1848 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v20".to_vec())))])]),
1849 )
1850 .unwrap();
1851 storage.set(
1852 CommitVersion(3),
1853 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v3".to_vec())))])]),
1854 )
1855 .unwrap();
1856
1857 let (to_persist, to_drop, _consumed, _more) =
1858 storage.collect_evictable_below(EntryKind::Multi, CommitVersion(5), UNBOUNDED);
1859 let dropped: HashSet<CommitVersion> = to_drop.iter().map(|e| e.version).collect();
1860 assert_eq!(
1861 dropped,
1862 HashSet::from([CommitVersion(3)]),
1863 "the out-of-order v3 must be selected; v20 stays resident"
1864 );
1865 assert_eq!(to_persist.len(), 1);
1866 assert_eq!(to_persist[0].1, CommitVersion(3), "the aged-out v3 is the value persisted");
1867 }
1868
1869 #[test]
1870 fn byte_tally_matches_a_full_walk_across_mixed_mutations() {
1871 let storage = CommitStore::new();
1874 let k1 = EncodedKey::new(b"key-one");
1875 let k2 = EncodedKey::new(b"key-two");
1876
1877 for v in 1..=3u64 {
1878 storage.set(
1879 CommitVersion(v),
1880 HashMap::from([(
1881 EntryKind::Multi,
1882 vec![(k1.clone(), Some(CowVec::new(format!("value-{v}").into_bytes())))],
1883 )]),
1884 )
1885 .unwrap();
1886 }
1887 storage.set(
1888 CommitVersion(5),
1889 HashMap::from([(EntryKind::Multi, vec![(k2.clone(), Some(CowVec::new(b"x".to_vec())))])]),
1890 )
1891 .unwrap();
1892 storage.set(
1893 CommitVersion(2),
1894 HashMap::from([(EntryKind::Multi, vec![(k2.clone(), Some(CowVec::new(b"older".to_vec())))])]),
1895 )
1896 .unwrap();
1897 storage.set(CommitVersion(6), HashMap::from([(EntryKind::Multi, vec![(k2.clone(), None)])])).unwrap();
1898
1899 storage.compact(HashMap::from([(EntryKind::Multi, vec![(k1.clone(), CommitVersion(3))])])).unwrap();
1900 storage.compact(HashMap::from([(EntryKind::Multi, vec![(k2.clone(), CommitVersion(2))])])).unwrap();
1901
1902 let entry = storage.inner.entries.data.get(&EntryKind::Multi).unwrap();
1903 let active = entry.active.read();
1904 let closed = entry.closed_snapshot();
1905 let mut walked_current = 0u64;
1906 let mut walked_historical = 0u64;
1907 for rows in iter::once(active.rows()).chain(closed.iter().map(|map| map.rows())) {
1908 for (key, versions) in rows.iter() {
1909 let newest = versions.keys().next().map(|Reverse(version)| *version);
1910 for (Reverse(version), value) in versions.iter() {
1911 if newest == Some(*version) {
1912 walked_current += entry_bytes(key, value);
1913 } else {
1914 walked_historical += entry_bytes(key, value);
1915 }
1916 }
1917 }
1918 }
1919 drop(active);
1920
1921 assert!(walked_current > 0, "precondition: the scenario must leave current entries behind");
1922 assert!(walked_historical > 0, "precondition: the scenario must leave historical entries behind");
1923 assert_eq!(
1924 storage.current_resident_bytes().as_bytes(),
1925 walked_current,
1926 "the incremental current tally must equal an exhaustive walk of the newest version of every key"
1927 );
1928 assert_eq!(
1929 storage.historical_resident_bytes().as_bytes(),
1930 walked_historical,
1931 "the incremental historical tally must equal an exhaustive walk of every superseded version"
1932 );
1933 }
1934
1935 #[test]
1936 fn byte_tally_nets_to_zero_when_the_buffer_is_fully_drained() {
1937 let storage = CommitStore::new();
1940 let key = EncodedKey::new(b"k");
1941 for v in 1..=4u64 {
1942 storage.set(
1943 CommitVersion(v),
1944 HashMap::from([(
1945 EntryKind::Multi,
1946 vec![(key.clone(), Some(CowVec::new(format!("v{v}").into_bytes())))],
1947 )]),
1948 )
1949 .unwrap();
1950 }
1951 storage.seal_for_flush(EntryKind::Multi, CommitVersion(u64::MAX));
1952 assert!(storage.current_resident_bytes().as_bytes() > 0);
1953 assert!(storage.historical_resident_bytes().as_bytes() > 0);
1954
1955 storage.compact(HashMap::from([(EntryKind::Multi, vec![(key.clone(), CommitVersion(4))])])).unwrap();
1956 storage.compact(HashMap::from([(
1957 EntryKind::Multi,
1958 vec![
1959 (key.clone(), CommitVersion(1)),
1960 (key.clone(), CommitVersion(2)),
1961 (key.clone(), CommitVersion(3)),
1962 ],
1963 )]))
1964 .unwrap();
1965
1966 assert_eq!(
1967 storage.current_resident_bytes(),
1968 ByteSize::ZERO,
1969 "draining every entry must return the current tally to zero"
1970 );
1971 assert_eq!(
1972 storage.historical_resident_bytes(),
1973 ByteSize::ZERO,
1974 "draining every entry must return the historical tally to zero"
1975 );
1976 }
1977
1978 #[test]
1979 fn clear_table_resets_the_byte_tally() {
1980 let storage = CommitStore::new();
1981 let key = EncodedKey::new(b"k");
1982 storage.set(
1983 CommitVersion(1),
1984 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v".to_vec())))])]),
1985 )
1986 .unwrap();
1987 storage.set(
1988 CommitVersion(2),
1989 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"w".to_vec())))])]),
1990 )
1991 .unwrap();
1992 assert!(storage.current_resident_bytes().as_bytes() > 0);
1993 assert!(storage.historical_resident_bytes().as_bytes() > 0);
1994
1995 storage.clear_table(EntryKind::Multi).unwrap();
1996 assert_eq!(
1997 storage.current_resident_bytes(),
1998 ByteSize::ZERO,
1999 "clearing a table must zero its byte tally, not leak it"
2000 );
2001 assert_eq!(storage.historical_resident_bytes(), ByteSize::ZERO);
2002 }
2003
2004 #[test]
2005 fn an_empty_buffer_has_no_oldest_pending_version() {
2006 let storage = CommitStore::new();
2010
2011 assert_eq!(storage.oldest_pending_version(), None);
2012 }
2013
2014 #[test]
2015 fn oldest_pending_version_is_the_minimum_across_every_entry_kind() {
2016 let storage = CommitStore::new();
2020 let key = EncodedKey::new(b"k");
2021
2022 storage.set(
2023 CommitVersion(40),
2024 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"late".to_vec())))])]),
2025 )
2026 .unwrap();
2027 storage.set(
2028 CommitVersion(7),
2029 HashMap::from([(
2030 EntryKind::Source(StorageId::Table(TableId(1)), EntryLayout::Row),
2031 vec![(key.clone(), Some(CowVec::new(b"early".to_vec())))],
2032 )]),
2033 )
2034 .unwrap();
2035 storage.set(
2036 CommitVersion(19),
2037 HashMap::from([(
2038 EntryKind::Source(StorageId::Table(TableId(2)), EntryLayout::Row),
2039 vec![(key.clone(), Some(CowVec::new(b"mid".to_vec())))],
2040 )]),
2041 )
2042 .unwrap();
2043
2044 assert_eq!(
2045 storage.oldest_pending_version(),
2046 Some(CommitVersion(7)),
2047 "the oldest un-flushed write in any keyspace is what bounds the durable frontier"
2048 );
2049 }
2050
2051 #[test]
2052 fn the_oldest_bucket_in_a_keyspace_wins_over_its_newer_ones() {
2053 let storage = CommitStore::new();
2057
2058 storage.set(
2059 CommitVersion(11),
2060 HashMap::from([(
2061 EntryKind::Multi,
2062 vec![(EncodedKey::new(b"late"), Some(CowVec::new(b"v".to_vec())))],
2063 )]),
2064 )
2065 .unwrap();
2066 storage.set(
2067 CommitVersion(4),
2068 HashMap::from([(
2069 EntryKind::Multi,
2070 vec![(EncodedKey::new(b"early"), Some(CowVec::new(b"v".to_vec())))],
2071 )]),
2072 )
2073 .unwrap();
2074
2075 assert_eq!(storage.oldest_pending_version(), Some(CommitVersion(4)));
2076 }
2077
2078 #[test]
2079 fn a_superseded_version_still_counts_as_pending_until_it_is_compacted_away() {
2080 let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
2084 let key = EncodedKey::new(b"k");
2085
2086 storage.set(
2087 CommitVersion(3),
2088 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v3".to_vec())))])]),
2089 )
2090 .unwrap();
2091 storage.set(
2092 CommitVersion(9),
2093 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v9".to_vec())))])]),
2094 )
2095 .unwrap();
2096
2097 assert_eq!(storage.oldest_pending_version(), Some(CommitVersion(3)));
2098
2099 storage.compact(HashMap::from([(EntryKind::Multi, vec![(key.clone(), CommitVersion(3))])])).unwrap();
2100
2101 assert_eq!(
2102 storage.oldest_pending_version(),
2103 Some(CommitVersion(9)),
2104 "once the sweep drains v3 the frontier may advance to the next un-flushed write"
2105 );
2106 }
2107
2108 #[test]
2109 fn draining_the_buffer_clears_the_oldest_pending_version() {
2110 let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
2114 let key = EncodedKey::new(b"k");
2115
2116 storage.set(
2117 CommitVersion(5),
2118 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v".to_vec())))])]),
2119 )
2120 .unwrap();
2121 storage.compact(HashMap::from([(EntryKind::Multi, vec![(key.clone(), CommitVersion(5))])])).unwrap();
2122
2123 assert_eq!(storage.oldest_pending_version(), None);
2124 }
2125
2126 #[test]
2127 fn a_default_buffer_round_trips_a_write() {
2128 let storage = CommitStore::new();
2129
2130 let key = EncodedKey::new(b"key");
2131 let version = CommitVersion(1);
2132
2133 storage.set(
2134 version,
2135 HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"value".to_vec())))])]),
2136 )
2137 .unwrap();
2138 assert_eq!(
2139 storage.get(EntryKind::Multi, &key, version).unwrap().value().as_deref(),
2140 Some(b"value".as_slice())
2141 );
2142 }
2143
2144 #[test]
2145 fn a_default_buffer_scans_every_key_at_the_written_version() {
2146 let storage = CommitStore::new();
2147
2148 let version = CommitVersion(1);
2149 storage.set(
2150 version,
2151 HashMap::from([(
2152 EntryKind::Multi,
2153 vec![
2154 (EncodedKey::new(b"a"), Some(CowVec::new(b"1".to_vec()))),
2155 (EncodedKey::new(b"b"), Some(CowVec::new(b"2".to_vec()))),
2156 (EncodedKey::new(b"c"), Some(CowVec::new(b"3".to_vec()))),
2157 ],
2158 )]),
2159 )
2160 .unwrap();
2161
2162 let mut cursor = RangeCursor::new();
2163 let batch = storage
2164 .range_next(
2165 EntryKind::Multi,
2166 &mut cursor,
2167 Bound::Unbounded,
2168 Bound::Unbounded,
2169 MultiVersionScope::AsOf {
2170 read: version,
2171 },
2172 100,
2173 )
2174 .unwrap();
2175
2176 assert_eq!(batch.entries.len(), 3);
2177 assert!(!batch.has_more);
2178 assert!(cursor.is_exhausted());
2179 }
2180
2181 #[test]
2182 fn a_point_read_finds_the_newest_version_at_or_below_the_asked_one_across_closed_maps() {
2183 let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
2186 let k = EncodedKey::new(b"k");
2187 for (version, value) in [(5u64, b"v5".as_slice()), (1, b"v1"), (9, b"v9")] {
2188 storage.set(
2189 CommitVersion(version),
2190 HashMap::from([(
2191 EntryKind::Multi,
2192 vec![(k.clone(), Some(CowVec::new(value.to_vec())))],
2193 )]),
2194 )
2195 .unwrap();
2196 }
2197 let entry = storage.inner.entries.data.get(&EntryKind::Multi).unwrap();
2198 assert_eq!(entry.closed.read().len(), 3, "precondition: one closed map per write");
2199
2200 let read = |version: u64| match storage.get(EntryKind::Multi, b"k", CommitVersion(version)).unwrap() {
2201 VersionedGetResult::Value {
2202 value,
2203 version,
2204 } => Some((version.0, value.to_vec())),
2205 VersionedGetResult::Tombstone => panic!("no tombstone was written"),
2206 VersionedGetResult::NotFound => None,
2207 };
2208 assert_eq!(read(7), Some((5, b"v5".to_vec())));
2209 assert_eq!(
2210 read(5),
2211 Some((5, b"v5".to_vec())),
2212 "a read at exactly a map's oldest version keeps that map"
2213 );
2214 assert_eq!(read(3), Some((1, b"v1".to_vec())));
2215 assert_eq!(read(100), Some((9, b"v9".to_vec())));
2216 assert_eq!(read(0), None);
2217 assert!(storage.contains(EntryKind::Multi, b"k", CommitVersion(7)).unwrap());
2218 assert!(!storage.contains(EntryKind::Multi, b"k", CommitVersion(0)).unwrap());
2219 }
2220
2221 #[test]
2222 fn the_key_count_follows_writes_closes_compaction_and_clearing() {
2223 let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
2226 let walked = |storage: &CommitStore| -> u64 {
2227 let entry = storage.inner.entries.data.get(&EntryKind::Multi).unwrap();
2228 let active = entry.active.read().rows().key_count();
2229 let closed: usize = entry.closed.read().iter().map(|map| map.rows().key_count()).sum();
2230 (active + closed) as u64
2231 };
2232 seed(&storage, 1, &[keyed("a", b"a1"), keyed("b", b"b1")]);
2233 assert_eq!(storage.estimated_current_count(EntryKind::Multi).unwrap(), 2);
2234 seed(&storage, 2, &[keyed("a", b"a2")]);
2235 assert_eq!(
2236 storage.estimated_current_count(EntryKind::Multi).unwrap(),
2237 3,
2238 "a key rewritten into a new map counts once per map"
2239 );
2240 assert_eq!(storage.estimated_current_count(EntryKind::Multi).unwrap(), walked(&storage));
2241
2242 storage.compact(HashMap::from([(EntryKind::Multi, vec![(EncodedKey::new(b"a"), CommitVersion(1))])]))
2243 .unwrap();
2244 assert_eq!(storage.estimated_current_count(EntryKind::Multi).unwrap(), 2);
2245 assert_eq!(storage.estimated_current_count(EntryKind::Multi).unwrap(), walked(&storage));
2246
2247 storage.clear_table(EntryKind::Multi).unwrap();
2248 assert_eq!(storage.estimated_current_count(EntryKind::Multi).unwrap(), 0);
2249
2250 let same_map = CommitStore::new();
2251 seed(&same_map, 1, &[keyed("a", b"a1")]);
2252 seed(&same_map, 2, &[keyed("a", b"a2")]);
2253 assert_eq!(
2254 same_map.estimated_current_count(EntryKind::Multi).unwrap(),
2255 1,
2256 "a rewrite inside the active map adds no key"
2257 );
2258 }
2259
2260 #[test]
2261 fn compaction_never_alters_a_map_a_reader_still_holds() {
2262 let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
2265 seed(&storage, 1, &[keyed("a", b"a1")]);
2266 seed(&storage, 2, &[keyed("a", b"a2")]);
2267 let entry = storage.inner.entries.data.get(&EntryKind::Multi).unwrap();
2268 let held = entry.closed_snapshot();
2269
2270 storage.compact(HashMap::from([(EntryKind::Multi, vec![(EncodedKey::new(b"a"), CommitVersion(1))])]))
2271 .unwrap();
2272
2273 let held_versions: usize = held
2274 .iter()
2275 .map(|map| map.rows().iter().map(|(_, versions)| versions.len()).sum::<usize>())
2276 .sum();
2277 assert_eq!(held_versions, 2, "the held snapshot still sees v1");
2278 assert_eq!(storage.get_all_versions(EntryKind::Multi, b"a").unwrap().len(), 1, "the store dropped v1");
2279 }
2280
2281 #[test]
2282 fn the_oldest_pending_version_rises_when_the_oldest_row_leaves_a_map_that_keeps_others() {
2283 let storage = CommitStore::new();
2287 seed(&storage, 3, &[keyed("a", b"a3")]);
2288 seed(&storage, 5, &[keyed("b", b"b5")]);
2289 assert_eq!(storage.oldest_pending_version(), Some(CommitVersion(3)));
2290
2291 storage.compact(HashMap::from([(EntryKind::Multi, vec![(EncodedKey::new(b"a"), CommitVersion(3))])]))
2292 .unwrap();
2293
2294 assert_eq!(storage.oldest_pending_version(), Some(CommitVersion(5)));
2295 }
2296}