use super::{Table, Writer};
use crate::table::RecoverParams;
use crate::{Cache, DescriptorTable, InternalValue, SeqNo, ValueType, fs::StdFs, hash::hash64};
use alloc::sync::Arc;
use core::sync::atomic::Ordering::Relaxed;
use tempfile::tempdir;
use test_log::test;
fn cold_table(dir: &std::path::Path, shape: impl Fn(Writer) -> Writer) -> crate::Result<Table> {
let file = dir.join("table");
let fs: Arc<dyn crate::fs::Fs> = Arc::new(StdFs);
let mut writer = shape(Writer::new(file.clone(), 0, 0, Arc::clone(&fs))?);
for i in (0u32..1000).step_by(2) {
writer.write(InternalValue::from_components(
format!("key{i:06}").into_bytes(),
b"v".to_vec(),
1,
ValueType::Value,
))?;
}
let (_, checksum) = writer.finish()?.expect("the keys make a table");
let mut params = RecoverParams::new(
file,
checksum,
0,
fs,
crate::comparator::default_comparator(),
Arc::new(Cache::with_capacity_bytes(0)),
);
params.descriptor_table = Some(Arc::new(DescriptorTable::new(10)));
params.pin_filter = false;
params.pin_index = false;
Table::recover(params)
}
fn descriptor_accesses(table: &Table) -> usize {
table.metrics.table_file_cached_count() + table.metrics.table_file_opened_count()
}
#[test]
fn descriptor_counts_split_cache_hits_from_opens() -> crate::Result<()> {
let dir = tempdir()?;
let table = cold_table(dir.path(), |w| w)?;
let (cached, opened) = (
table.metrics.table_file_cached_count(),
table.metrics.table_file_opened_count(),
);
let key = b"key000500";
assert!(table.get(key, SeqNo::MAX, hash64(key))?.is_some());
assert_eq!(
table.metrics.table_file_opened_count() - opened,
1,
"opened"
);
assert_eq!(
table.metrics.table_file_cached_count() - cached,
0,
"cached"
);
assert!(table.get(key, SeqNo::MAX, hash64(key))?.is_some());
assert_eq!(
table.metrics.table_file_opened_count() - opened,
1,
"opened"
);
assert_eq!(
table.metrics.table_file_cached_count() - cached,
1,
"cached"
);
Ok(())
}
type Shape = (&'static str, fn(Writer) -> Writer);
fn shapes() -> Vec<Shape> {
let mut shapes: Vec<Shape> = vec![
("monolithic", |w| w),
("partitioned", |w| {
w.use_data_block_size(64)
.use_partitioned_filter()
.use_filter_partition_size(64)
.use_partitioned_index()
.use_index_partition_size(64)
}),
];
#[cfg(feature = "columnar")]
shapes.push(("columnar", |w| w.use_columnar(true)));
shapes
}
#[test]
fn cold_point_reads_take_the_descriptor_once_per_lookup() -> crate::Result<()> {
let key = b"key000500";
let hash = hash64(key);
for (name, shape) in shapes() {
let dir = tempdir()?;
let table = cold_table(dir.path(), shape)?;
let before = descriptor_accesses(&table);
let blocks = table.metrics.block_load_io_count();
assert!(table.get(key, SeqNo::MAX, hash)?.is_some(), "{name}: get");
assert!(
table.metrics.block_load_io_count() - blocks >= 3,
"{name}: the lookup did not read filter, index and data from the file"
);
assert_eq!(descriptor_accesses(&table) - before, 1, "{name}: get");
let before = descriptor_accesses(&table);
assert!(
table.get_value(key, SeqNo::MAX, hash)?.is_some(),
"{name}: get_value"
);
assert_eq!(descriptor_accesses(&table) - before, 1, "{name}: get_value");
let before = descriptor_accesses(&table);
assert!(
table.get_with_block(key, SeqNo::MAX, hash)?.is_some(),
"{name}: get_with_block"
);
assert_eq!(
descriptor_accesses(&table) - before,
1,
"{name}: get_with_block"
);
let keys: Vec<(&[u8], u64)> = [b"key000002".as_slice(), b"key000500", b"key000998"]
.into_iter()
.map(|k| (k, hash64(k)))
.collect();
let before = descriptor_accesses(&table);
let found = table.batch_get(&keys, SeqNo::MAX)?;
assert!(found.iter().all(Option::is_some), "{name}: batch_get");
assert_eq!(descriptor_accesses(&table) - before, 1, "{name}: batch_get");
}
Ok(())
}
fn scan_shapes() -> Vec<Shape> {
let mut shapes: Vec<Shape> = vec![
("monolithic", |w| w.use_data_block_size(64)),
("partitioned", |w| {
w.use_data_block_size(64)
.use_partitioned_index()
.use_index_partition_size(64)
}),
];
#[cfg(feature = "columnar")]
shapes.push(("columnar", |w| w.use_columnar(true).use_row_group_size(256)));
shapes
}
fn cold_scan_table(
dir: &std::path::Path,
name: &str,
shape: fn(Writer) -> Writer,
) -> crate::Result<Table> {
let table = cold_table(dir, shape)?;
assert!(
table.metadata.data_block_count >= 8,
"{name}: the case under test: {} blocks, not several",
table.metadata.data_block_count
);
Ok(table)
}
#[test]
fn a_cold_scan_takes_the_descriptor_once() -> crate::Result<()> {
for (name, shape) in scan_shapes() {
let dir = tempdir()?;
let table = cold_scan_table(dir.path(), name, shape)?;
let before = descriptor_accesses(&table);
assert_eq!(table.iter().count(), 500, "{name}: forward");
assert_eq!(descriptor_accesses(&table) - before, 1, "{name}: forward");
let before = descriptor_accesses(&table);
assert_eq!(table.iter().rev().count(), 500, "{name}: backward");
assert_eq!(descriptor_accesses(&table) - before, 1, "{name}: backward");
}
Ok(())
}
#[test]
fn a_scan_takes_the_descriptor_again_once_the_cache_drops_it() -> crate::Result<()> {
for (name, shape) in scan_shapes() {
let dir = tempdir()?;
let table = cold_scan_table(dir.path(), name, shape)?;
let before = descriptor_accesses(&table);
let mut iter = table.iter();
let head = iter.by_ref().take(100).count();
assert_eq!(descriptor_accesses(&table) - before, 1, "{name}: head");
table.file_accessor.remove_for_table(&table.global_id());
let blocks = table.metrics.block_load_io_count();
assert_eq!(head + iter.count(), 500, "{name}: rows");
assert!(
table.metrics.block_load_io_count() > blocks,
"{name}: the case under test: the scan reads from the file after the drop"
);
assert_eq!(descriptor_accesses(&table) - before, 2, "{name}: after");
}
Ok(())
}
#[test]
fn a_scan_does_not_keep_the_descriptor_open() -> crate::Result<()> {
for (name, shape) in scan_shapes() {
let dir = tempdir()?;
let table = cold_scan_table(dir.path(), name, shape)?;
let mut iter = table.iter();
assert!(iter.next().is_some(), "{name}: first row");
let cached = table
.file_accessor
.as_descriptor_table()
.and_then(|d| d.access_for_table(&table.global_id()))
.expect("the scan's read cached the descriptor");
assert_eq!(
Arc::strong_count(&cached),
2,
"{name}: the cache and this test hold the descriptor, the scan does not"
);
drop(cached);
assert_eq!(iter.count(), 499, "{name}: rest");
}
Ok(())
}
#[test]
fn cold_block_task_plan_takes_the_descriptor_once() -> crate::Result<()> {
let keys: Vec<(&[u8], u64)> = [b"key000002".as_slice(), b"key000500", b"key000998"]
.into_iter()
.map(|k| (k, hash64(k)))
.collect();
for (name, shape) in shapes() {
let dir = tempdir()?;
let table = cold_table(dir.path(), shape)?;
let before = descriptor_accesses(&table);
let filters = table.metrics.filter_block_load_io.load(Relaxed);
let indexes = table.metrics.index_block_load_io.load(Relaxed);
let plan = table.plan_block_tasks(
&keys,
SeqNo::MAX,
&mut crate::table::probe_stats::PlanCounts::default(),
)?;
assert!(plan.is_some(), "{name}: the keys are planned");
assert!(
table.metrics.filter_block_load_io.load(Relaxed) > filters
&& table.metrics.index_block_load_io.load(Relaxed) > indexes,
"{name}: the plan did not read its filter and index blocks from the file"
);
assert_eq!(descriptor_accesses(&table) - before, 1, "{name}");
}
Ok(())
}
#[test]
fn cold_lookups_that_miss_take_the_descriptor_at_most_once() -> crate::Result<()> {
for (name, shape) in shapes() {
let dir = tempdir()?;
let table = cold_table(dir.path(), shape)?;
for i in (1u32..1000).step_by(50) {
let key = format!("key{i:06}");
let before = descriptor_accesses(&table);
assert!(
table
.get(key.as_bytes(), SeqNo::MAX, hash64(key.as_bytes()))?
.is_none(),
"{name}: {key} is absent"
);
assert!(
descriptor_accesses(&table) - before <= 1,
"{name}: {key} took the descriptor more than once"
);
}
}
Ok(())
}