pub(crate) mod probes;
use std::{ops::Bound, sync::Arc};
use bytes::Bytes;
use futures::{StreamExt, TryStreamExt, stream};
use slatedb::{
ByteRangeBounds, DbIterator, DbReader, DbTransaction, IterationOrder, KeyValue,
config::ScanOptions,
};
use crate::store::key;
const SCAN_READ_AHEAD_BYTES: usize = 8 * 1024 * 1024;
const SCAN_FETCH_TASKS: usize = 32;
pub(crate) const SCAN_SPLIT: usize = 8;
const SCAN_SPLIT_BYTES: usize = SCAN_READ_AHEAD_BYTES;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ScanShape {
Bulk,
Streaming,
Probe,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ScanOrder {
Ascending,
Descending,
}
impl ScanOrder {
pub(crate) fn from_reverse(reverse: bool) -> Self {
if reverse {
Self::Descending
} else {
Self::Ascending
}
}
fn iteration_order(self) -> IterationOrder {
match self {
Self::Ascending => IterationOrder::Ascending,
Self::Descending => IterationOrder::Descending,
}
}
}
impl ScanShape {
fn options(self, order: ScanOrder) -> ScanOptions {
ScanOptions {
read_ahead_bytes: if self == Self::Streaming {
1
} else {
SCAN_READ_AHEAD_BYTES
},
max_fetch_tasks: if self == Self::Streaming {
1
} else {
SCAN_FETCH_TASKS
},
cache_blocks: matches!(self, Self::Probe),
order: order.iteration_order(),
..ScanOptions::default()
}
}
}
#[derive(Clone, Copy)]
pub(crate) enum ReadHandle<'a> {
Tx(&'a DbTransaction),
Reader(&'a DbReader),
}
impl ReadHandle<'_> {
pub(crate) async fn get<K: AsRef<[u8]> + Send>(
&self,
key: K,
) -> Result<Option<Bytes>, slatedb::Error> {
match self {
Self::Tx(tx) => tx.get(key).await,
Self::Reader(reader) => reader.get(key).await,
}
}
pub(crate) fn is_isolated(&self) -> bool {
matches!(self, Self::Tx(_))
}
pub(crate) async fn scan_prefix<P, T>(
&self,
prefix: P,
subrange: T,
shape: ScanShape,
) -> Result<DbIterator, slatedb::Error>
where
P: AsRef<[u8]> + Send,
T: ByteRangeBounds + Send,
{
self.scan_prefix_ordered(prefix, subrange, shape, ScanOrder::Ascending)
.await
}
pub(crate) async fn scan_prefix_ordered<P, T>(
&self,
prefix: P,
subrange: T,
shape: ScanShape,
order: ScanOrder,
) -> Result<DbIterator, slatedb::Error>
where
P: AsRef<[u8]> + Send,
T: ByteRangeBounds + Send,
{
let options = shape.options(order);
match self {
Self::Tx(tx) => {
tx.scan_prefix_with_options(prefix, subrange, &options)
.await
}
Self::Reader(reader) => {
reader
.scan_prefix_with_options(prefix, subrange, &options)
.await
}
}
}
async fn highest_suffix(
&self,
prefix: &[u8],
shape: ScanShape,
) -> Result<Option<Vec<u8>>, slatedb::Error> {
let mut descending = self
.scan_prefix_ordered(prefix, .., shape, ScanOrder::Descending)
.await?;
Ok(descending
.next()
.await?
.map(|entry| entry.key.get(prefix.len()..).unwrap_or(&[]).to_vec()))
}
async fn collect_range(
&self,
prefix: &[u8],
range: key::SuffixRange,
shape: ScanShape,
) -> Result<Vec<KeyValue>, slatedb::Error> {
let mut iterator = self.scan_prefix(prefix, range, shape).await?;
let mut entries = Vec::new();
while let Some(entry) = iterator.next().await? {
entries.push(entry);
}
Ok(entries)
}
pub(crate) async fn scan_prefix_split_over(
&self,
prefix: &[u8],
splits: &[Vec<u8>],
shape: ScanShape,
threshold: usize,
) -> Result<Vec<KeyValue>, slatedb::Error> {
let suffix_of = |bytes: &[u8]| bytes.get(prefix.len()..).unwrap_or(&[]).to_vec();
let mut entries = Vec::new();
let mut cursor = Bound::Unbounded;
'resume: loop {
let mut iterator = self
.scan_prefix(prefix, (cursor.clone(), Bound::Unbounded), shape)
.await?;
let mut streamed = (None, 0_usize);
while let Some(entry) = iterator.next().await? {
let split = splits.iter().find(|split| entry.key.starts_with(split));
if split != streamed.0 {
streamed = (split, 0);
}
streamed.1 = streamed
.1
.saturating_add(entry.key.len() + entry.value.len());
let last = suffix_of(&entry.key);
entries.push(entry);
let Some(split) = split.filter(|_| streamed.1 > threshold) else {
continue;
};
let split_suffix = suffix_of(split);
let end = key::increment_prefix(split_suffix.clone());
let upper = end.clone().map_or(Bound::Unbounded, Bound::Excluded);
let points = match self.highest_suffix(split, shape).await? {
Some(high) => {
let low = last.get(split_suffix.len()..).unwrap_or(&[]);
key::even_points_between(low, &high, SCAN_SPLIT)
.into_iter()
.map(|point| [split_suffix.as_slice(), &point].concat())
.collect()
}
None => Vec::new(),
};
let ranges = key::split_between(Bound::Excluded(last), upper, points);
let remainder = stream::iter(ranges)
.map(|range| self.collect_range(prefix, range, shape))
.buffered(SCAN_SPLIT)
.try_concat()
.await?;
entries.extend(remainder);
match end {
Some(end) => {
cursor = Bound::Included(end);
continue 'resume;
}
None => break 'resume,
}
}
break;
}
Ok(entries)
}
pub(crate) async fn scan_prefix_split(
&self,
prefix: &[u8],
splits: &[Vec<u8>],
shape: ScanShape,
) -> Result<Vec<KeyValue>, slatedb::Error> {
self.scan_prefix_split_over(prefix, splits, shape, SCAN_SPLIT_BYTES)
.await
}
}
pub(crate) enum ReadSession {
Tx(DbTransaction),
Reader(Arc<DbReader>),
}
impl ReadSession {
pub(crate) fn handle(&self) -> ReadHandle<'_> {
match self {
Self::Tx(tx) => ReadHandle::Tx(tx),
Self::Reader(reader) => ReadHandle::Reader(reader),
}
}
pub(crate) fn finish(self) {
if let Self::Tx(tx) = self {
tx.rollback();
}
}
}
#[cfg(test)]
mod tests {
use object_store::memory::InMemory;
use slatedb::IsolationLevel;
use super::*;
use crate::store::{
key::{EntityKey, EntityKind, Key, Subspace},
open::StoreBuilder,
};
#[tokio::test]
async fn a_split_scan_yields_what_one_iterator_yields() {
let (db, _) = StoreBuilder::new("t", Arc::new(InMemory::new()))
.open_writer()
.await
.unwrap();
let tx = db.begin(IsolationLevel::Snapshot).await.unwrap();
for table_id in [3_u64, 7, 9] {
tx.put(Key::current(EntityKey::Table { table_id }).encode(), b"t")
.unwrap();
tx.put(
Key::current(EntityKey::TableStats { table_id }).encode(),
b"s",
)
.unwrap();
for column_id in 0..table_id {
tx.put(
Key::current(EntityKey::Column {
table_id,
column_id,
})
.encode(),
b"c",
)
.unwrap();
}
for data_file_id in 0..(table_id * 30) {
tx.put(
Key::current(EntityKey::File {
table_id,
data_file_id,
})
.encode(),
b"f",
)
.unwrap();
}
}
tx.put(
Key::current(EntityKey::DeleteFile {
table_id: 3,
delete_file_id: 1,
})
.encode(),
b"d",
)
.unwrap();
let write = tx.commit().await.unwrap().unwrap();
write.await_durable().await.unwrap();
let tx = db.begin(IsolationLevel::Snapshot).await.unwrap();
let handle = ReadHandle::Tx(&tx);
let prefix = key::subspace_prefix(Subspace::Current);
let plain = handle
.collect_range(
&prefix,
(Bound::Unbounded, Bound::Unbounded),
ScanShape::Bulk,
)
.await
.unwrap();
assert_eq!(plain.len(), 3 + 3 + (3 + 7 + 9) + 30 * (3 + 7 + 9) + 1);
let splits = key::SPLIT_SCAN_KINDS.map(key::current_entity_kind_prefix);
for threshold in [0, 64, usize::MAX] {
let split = handle
.scan_prefix_split_over(&prefix, &splits, ScanShape::Bulk, threshold)
.await
.unwrap();
assert_eq!(split.len(), plain.len(), "threshold {threshold}");
assert!(
split
.iter()
.zip(&plain)
.all(|(split, plain)| split.key == plain.key && split.value == plain.value),
"threshold {threshold} reordered or altered the scan"
);
}
let files = key::current_entity_kind_prefix(EntityKind::File);
let plain_files = handle
.collect_range(
&files,
(Bound::Unbounded, Bound::Unbounded),
ScanShape::Bulk,
)
.await
.unwrap();
let split_files = handle
.scan_prefix_split_over(&files, std::slice::from_ref(&files), ScanShape::Bulk, 0)
.await
.unwrap();
assert_eq!(split_files.len(), plain_files.len());
assert!(
split_files
.iter()
.zip(&plain_files)
.all(|(split, plain)| split.key == plain.key)
);
tx.rollback();
db.close().await.unwrap();
}
#[test]
fn bulk_scans_read_ahead_and_admit_nothing() {
let options = ScanShape::Bulk.options(ScanOrder::Ascending);
assert_eq!(options.read_ahead_bytes, SCAN_READ_AHEAD_BYTES);
assert_eq!(options.max_fetch_tasks, SCAN_FETCH_TASKS);
assert!(!options.cache_blocks);
}
#[test]
fn probe_scans_admit_their_blocks() {
let options = ScanShape::Probe.options(ScanOrder::Ascending);
assert_eq!(options.read_ahead_bytes, SCAN_READ_AHEAD_BYTES);
assert_eq!(options.max_fetch_tasks, SCAN_FETCH_TASKS);
assert!(options.cache_blocks);
}
#[test]
fn descending_probes_request_descending_iteration() {
let options = ScanShape::Probe.options(ScanOrder::Descending);
assert!(matches!(options.order, IterationOrder::Descending));
}
}