use super::Strategy;
use crate::{
AbstractTree, Config, KvSeparationOptions, SequenceNumberCounter, time::with_test_clock,
};
use std::sync::Arc;
#[test]
fn fifo_empty_levels() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
let fifo = Arc::new(Strategy::new(1, None));
tree.compact(fifo, 0)?;
assert_eq!(0, tree.table_count());
Ok(())
}
#[test]
fn fifo_below_limit() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
for i in 0..4u8 {
tree.insert([b'k', i].as_slice(), "v", u64::from(i));
tree.flush_active_memtable(u64::from(i))?;
}
let before = tree.table_count();
let fifo = Arc::new(Strategy::new(u64::MAX, None));
tree.compact(fifo, 4)?;
assert_eq!(before, tree.table_count());
Ok(())
}
#[test]
fn fifo_more_than_limit() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
for i in 0..4u8 {
tree.insert([b'k', i].as_slice(), "v", u64::from(i));
tree.flush_active_memtable(u64::from(i))?;
}
let before = tree.table_count();
let fifo = Arc::new(Strategy::new(1, None));
tree.compact(fifo, 4)?;
assert!(tree.table_count() < before);
Ok(())
}
#[test]
fn fifo_more_than_limit_blobs() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1)))
.open()?;
for i in 0..3u8 {
tree.insert([b'k', i].as_slice(), "$", u64::from(i));
tree.flush_active_memtable(u64::from(i))?;
}
let before = tree.table_count();
let fifo = Arc::new(Strategy::new(1, None));
tree.compact(fifo, 3)?;
assert!(tree.table_count() < before);
Ok(())
}
#[test]
fn fifo_ttl() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
with_test_clock(|clock| {
clock.set_secs(1_000);
tree.insert("a", "1", 0);
tree.flush_active_memtable(0)?;
clock.set_secs(1_005);
tree.insert("b", "2", 1);
tree.flush_active_memtable(1)?;
clock.set_secs(1_011);
assert_eq!(2, tree.table_count());
let fifo = Arc::new(Strategy::new(u64::MAX, Some(10)));
tree.compact(fifo, 2)?;
assert_eq!(1, tree.table_count());
Ok(())
})
}
#[test]
fn fifo_overlapping_l0_compacts_without_panic_and_drops_oldest_first() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
with_test_clock(|_| {
tree.insert("a", "old", 0);
tree.insert("c", "old", 1);
tree.flush_active_memtable(1)?;
tree.insert("b", "new", 2);
tree.flush_active_memtable(2)?;
tree.compact(Arc::new(Strategy::new(u64::MAX, None)), 3)?;
assert_eq!(2, tree.table_count());
let newest_size = tree
.current_version()
.iter_tables()
.max_by_key(|t| t.get_highest_seqno())
.map(crate::table::Table::file_size)
.unwrap_or_default();
tree.compact(Arc::new(Strategy::new(newest_size, None)), 3)?;
assert_eq!(1, tree.table_count());
assert!(tree.get("b", 3)?.is_some(), "the newer table must survive");
assert!(tree.get("a", 3)?.is_none(), "the older table goes first");
Ok(())
})
}
#[test]
fn fifo_limit_applies_after_major_compaction() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
for i in 0..4u8 {
tree.insert([b'k', i].as_slice(), "v", u64::from(i));
tree.flush_active_memtable(u64::from(i))?;
}
tree.major_compact(u64::MAX, 0)?;
assert_eq!(0, tree.current_version().l0().table_count());
assert!(tree.table_count() > 0);
tree.compact(Arc::new(Strategy::new(1, None)), 4)?;
assert_eq!(
0,
tree.table_count(),
"tables below L0 must count against the limit"
);
Ok(())
}
#[test]
fn fifo_ttl_applies_after_major_compaction() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
with_test_clock(|clock| {
clock.set_secs(1_000);
for i in 0..3u8 {
tree.insert([b'k', i].as_slice(), "v", u64::from(i));
tree.flush_active_memtable(u64::from(i))?;
}
tree.major_compact(u64::MAX, 0)?;
clock.set_secs(1_011);
tree.compact(Arc::new(Strategy::new(u64::MAX, Some(10))), 3)?;
assert_eq!(0, tree.table_count(), "expired tables below L0 must drop");
Ok(())
})
}
#[test]
fn fifo_after_major_compaction_drops_oldest_data_first_for_decreasing_keys() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
let mut state = 0x9E37_79B9_7F4A_7C15u64;
let mut value = || {
(0..256)
.map(|_| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
state.to_be_bytes()[0]
})
.collect::<Vec<u8>>()
};
let keys = 400u32;
with_test_clock(|_| {
for key in (0..keys).rev() {
let seqno = u64::from(keys - 1 - key);
tree.insert(key.to_be_bytes().as_slice(), value(), seqno);
if key % 100 == 0 {
tree.flush_active_memtable(seqno)?;
}
}
let watermark = u64::from(keys);
tree.major_compact(16 * 1024, 0)?;
assert!(
tree.table_count() > 1,
"the compaction must produce several outputs"
);
let newest = tree
.current_version()
.iter_tables()
.max_by_key(|t| t.get_highest_seqno())
.map(crate::table::Table::file_size)
.unwrap_or_default();
tree.compact(Arc::new(Strategy::new(newest, None)), watermark)?;
assert!(
tree.get(0u32.to_be_bytes(), watermark)?.is_some(),
"the newest key must survive"
);
assert!(
tree.get((keys - 1).to_be_bytes(), watermark)?.is_none(),
"the oldest key goes first"
);
Ok(())
})
}
#[test]
fn fifo_choose_skips_hidden_tables_instead_of_panicking() -> crate::Result<()> {
use super::super::{Choice, CompactionStrategy, state::CompactionState};
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
with_test_clock(|_| {
for i in 0..3u8 {
tree.insert([b'k', i].as_slice(), "v", u64::from(i));
tree.flush_active_memtable(u64::from(i))?;
}
crate::Result::Ok(())
})?;
let version = tree.current_version();
let Some(newest) = version
.iter_tables()
.max_by_key(|t| t.get_highest_seqno())
.map(crate::table::Table::id)
else {
panic!("three tables were flushed");
};
let ids: Vec<_> = version.iter_tables().map(crate::table::Table::id).collect();
let mut state = CompactionState::default();
state.hidden_set_mut().hide([newest]);
let choice = Strategy::new(1, None).choose(&version, &Config::default(), &state);
let Choice::Drop(dropped) = choice else {
panic!("expected a drop of the older tables not held");
};
assert!(
!dropped.contains(&newest),
"a held table must not be dropped"
);
assert_eq!(2, dropped.len(), "both older tables go");
let mut all_held = CompactionState::default();
all_held.hidden_set_mut().hide(ids.iter().copied());
assert!(matches!(
Strategy::new(1, None).choose(&version, &Config::default(), &all_held),
Choice::DoNothing
));
Ok(())
}
#[test]
fn fifo_choose_drops_no_newer_table_while_the_oldest_is_held() -> crate::Result<()> {
use super::super::{Choice, CompactionStrategy, state::CompactionState};
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
with_test_clock(|_| {
for i in 0..3u8 {
tree.insert([b'k', i].as_slice(), "v", u64::from(i));
tree.flush_active_memtable(u64::from(i))?;
}
crate::Result::Ok(())
})?;
let version = tree.current_version();
let Some((oldest, oldest_size)) = version
.iter_tables()
.min_by_key(|t| t.get_highest_seqno())
.map(|t| (t.id(), t.file_size()))
else {
panic!("three tables were flushed");
};
let total = version
.iter_tables()
.map(crate::table::Table::file_size)
.sum::<u64>();
let mut state = CompactionState::default();
state.hidden_set_mut().hide([oldest]);
let choice =
Strategy::new(total - oldest_size, None).choose(&version, &Config::default(), &state);
assert!(
matches!(choice, Choice::DoNothing),
"no newer table may go while the oldest is held"
);
Ok(())
}
#[test]
fn fifo_after_a_seqno_zeroing_major_compaction_drops_oldest_data_first() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
let mut state = 0x9E37_79B9_7F4A_7C15u64;
let mut value = || {
(0..256)
.map(|_| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
state.to_be_bytes()[0]
})
.collect::<Vec<u8>>()
};
let keys = 400u32;
with_test_clock(|clock| {
for key in (0..keys).rev() {
let seqno = u64::from(keys - 1 - key);
tree.insert(key.to_be_bytes().as_slice(), value(), seqno);
if key % 100 == 0 {
clock.set_secs(1_000 + seqno / 100);
tree.flush_active_memtable(seqno)?;
}
}
let watermark = u64::from(keys);
tree.major_compact(16 * 1024, watermark)?;
let version = tree.current_version();
assert!(version.iter_tables().count() > 1, "several outputs");
assert!(
version.iter_tables().all(|t| t.get_highest_seqno() == 0),
"the compaction zeroed every sequence number"
);
let Some(newest_age) = version.iter_tables().map(|t| t.metadata.created_at).max() else {
panic!("the compaction wrote tables");
};
let newest = version
.iter_tables()
.filter(|t| t.metadata.created_at == newest_age)
.map(crate::table::Table::file_size)
.sum::<u64>();
drop(version);
tree.compact(Arc::new(Strategy::new(newest, None)), watermark)?;
assert!(
tree.get(0u32.to_be_bytes(), watermark)?.is_some(),
"the newest key must survive"
);
assert!(
tree.get((keys - 1).to_be_bytes(), watermark)?.is_none(),
"the oldest key goes first"
);
Ok(())
})
}
#[test]
fn fifo_ttl_counts_from_the_data_age_not_from_a_later_compaction() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
with_test_clock(|clock| {
clock.set_secs(1_000);
for i in 0..3u8 {
tree.insert([b'k', i].as_slice(), "v", u64::from(i));
tree.flush_active_memtable(u64::from(i))?;
}
clock.set_secs(1_009);
tree.major_compact(u64::MAX, 0)?;
clock.set_secs(1_011);
tree.compact(Arc::new(Strategy::new(u64::MAX, Some(10))), 3)?;
assert_eq!(
0,
tree.table_count(),
"data written at 1000s expires at 1010s whatever rewrote it"
);
Ok(())
})
}
#[test]
fn fifo_ttl_is_off_while_the_clock_reads_zero() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
with_test_clock(|clock| {
clock.set_secs(0);
for i in 0..3u8 {
tree.insert([b'k', i].as_slice(), "v", u64::from(i));
tree.flush_active_memtable(u64::from(i))?;
}
tree.major_compact(u64::MAX, 0)?;
tree.compact(Arc::new(Strategy::new(u64::MAX, Some(10))), 3)?;
assert!(
tree.table_count() > 0,
"no table may expire without a clock"
);
assert!(tree.get([b'k', 0].as_slice(), 3)?.is_some());
Ok(())
})
}
#[test]
fn fifo_ttl_spares_tables_stamped_before_the_clock_started() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
with_test_clock(|clock| {
clock.set_secs(0);
for i in 0..3u8 {
tree.insert([b'k', i].as_slice(), "v", u64::from(i));
tree.flush_active_memtable(u64::from(i))?;
}
tree.major_compact(u64::MAX, 0)?;
clock.set_secs(1_000_000);
tree.compact(Arc::new(Strategy::new(u64::MAX, Some(10))), 3)?;
assert!(
tree.get([b'k', 0].as_slice(), 3)?.is_some(),
"a table with no age must not expire once the clock starts"
);
Ok(())
})
}
#[test]
fn fifo_counts_a_shared_blob_file_freed_only_with_its_last_table() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1)))
.open()?;
let mut state = 0x2545_F491_4F6C_DD1Du64;
let mut value = || {
(0..256)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state.to_be_bytes()[0]
})
.collect::<Vec<u8>>()
};
let keys = 4_000u32;
for key in 0..keys {
tree.insert(key.to_be_bytes().as_slice(), value(), u64::from(key));
}
tree.flush_active_memtable(u64::from(keys))?;
tree.major_compact(4 * 1024, 0)?;
let disk = || {
let version = tree.current_version();
version
.iter_tables()
.map(crate::table::Table::file_size)
.sum::<u64>()
+ version.blob_files.on_disk_size()
};
let version = tree.current_version();
assert!(version.iter_tables().count() > 1, "several outputs");
assert_eq!(1, version.blob_files.len(), "sharing one blob file");
drop(version);
let limit = disk() / 2;
tree.compact(Arc::new(Strategy::new(limit, None)), u64::from(keys) + 1)?;
let after = disk();
assert!(
after <= limit,
"one round must bring the tree within its limit: {after} > {limit}"
);
Ok(())
}
#[test]
fn fifo_counts_no_blob_file_a_memtable_row_keeps() -> crate::Result<()> {
use crate::blob_tree::field_row::{Cell, FIRST_FIELD_COLUMN, Field};
let status = FIRST_FIELD_COLUMN;
let body_column = FIRST_FIELD_COLUMN + 1;
let dir = tempfile::tempdir()?;
let crate::AnyTree::Blob(tree) = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.with_kv_separation(Some(
KvSeparationOptions::default().separation_threshold(64),
))
.blob_compression(crate::CompressionType::None)
.open()?
else {
panic!("a tree with kv separation opens as a blob tree");
};
let body = vec![b'b'; 64 * 1024];
with_test_clock(|_| {
tree.insert_cells(
"doc",
&[
Field::bytes(status, b"draft"),
Field::bytes(body_column, &body),
],
0,
)?;
tree.flush_active_memtable(0)?;
tree.insert("z", "v", 1);
tree.flush_active_memtable(1)?;
crate::Result::Ok(())
})?;
let Some(row) = tree.get_cells("doc", 2)? else {
panic!("the row was flushed");
};
let Some(borrowed) = row
.fields()?
.into_iter()
.find(|field| field.column == body_column && matches!(field.cell, Cell::Ref(_)))
else {
panic!("the body was separated");
};
tree.insert_cells("doc", &[Field::bytes(status, b"final"), borrowed], 2)?;
drop(row);
let version = tree.current_version();
let blob_bytes = version.blob_files.on_disk_size();
let Some(newer) = version
.iter_tables()
.max_by_key(|table| table.get_highest_seqno())
.map(crate::table::Table::file_size)
else {
panic!("two tables were flushed");
};
drop(version);
let disk = || {
let version = tree.current_version();
version
.iter_tables()
.map(crate::table::Table::file_size)
.sum::<u64>()
+ version.blob_files.on_disk_size()
};
let limit = blob_bytes + newer / 2;
tree.compact(Arc::new(Strategy::new(limit, None)), 3)?;
let after = disk();
assert!(
after <= limit,
"one round must bring the tree within its limit: {after} > {limit}"
);
assert_eq!(1, tree.blob_file_count(), "the borrowed file stays");
Ok(())
}
#[test]
fn fifo_ttl_then_limit_additional_drops_blob_unit() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1)))
.open()?;
with_test_clock(|clock| {
clock.set_secs(1_000);
tree.insert("a", "$", 0);
tree.flush_active_memtable(0)?;
tree.insert("b", "$", 1);
tree.flush_active_memtable(1)?;
clock.set_secs(10_000_000);
let fifo = Arc::new(Strategy::new(1, Some(1)));
tree.compact(fifo, 2)?;
assert_eq!(0, tree.table_count());
Ok(())
})
}