use std::ops::Bound::{self, Excluded, Included, Unbounded};
use std::ops::RangeBounds;
use std::sync::Arc;
use bytes::Bytes;
use futures::stream::{self, StreamExt, TryStreamExt};
use serde::Serialize;
use crate::bytes_range::BytesRange;
use crate::db_state::{SortedRun, SsTableHandle, SsTableView};
use crate::error::SlateDBError;
use crate::flatbuffer_types::SsTableIndexOwned;
use crate::tablestore::TableStore;
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Subcompaction {
range: BytesRange,
output_ssts: Vec<SsTableHandle>,
}
impl Subcompaction {
pub(crate) fn new(range: BytesRange) -> Self {
Self {
range,
output_ssts: Vec::new(),
}
}
pub(crate) fn with_output_ssts(mut self, output_ssts: Vec<SsTableHandle>) -> Self {
self.output_ssts = output_ssts;
self
}
pub(crate) fn range(&self) -> &BytesRange {
&self.range
}
pub fn output_ssts(&self) -> &Vec<SsTableHandle> {
&self.output_ssts
}
pub(crate) fn set_output_ssts(&mut self, output_ssts: Vec<SsTableHandle>) {
assert!(
output_ssts.starts_with(self.output_ssts.as_slice()),
"new subcompaction output SSTs must always extend previous output SSTs"
);
self.output_ssts = output_ssts;
}
}
const MAX_ANCHORS_PER_SST: usize = 128;
const MAX_PLANNING_INDEX_READS: usize = 64;
pub(crate) async fn plan_subcompaction_ranges(
table_store: &Arc<TableStore>,
sst_views: &[SsTableView],
sorted_runs: &[SortedRun],
max_subcompactions: usize,
max_fetch_tasks: usize,
) -> Result<Vec<BytesRange>, SlateDBError> {
if max_subcompactions <= 1 {
return Ok(vec![BytesRange::unbounded()]);
}
let views: Vec<SsTableView> = sst_views
.iter()
.cloned()
.chain(
sorted_runs
.iter()
.flat_map(|sr| sr.sst_views.iter().cloned()),
)
.collect();
let min_range_bytes = views
.iter()
.map(|view| view.estimate_size())
.max()
.unwrap_or(0);
let sources = (sst_views.len() + sorted_runs.len()).max(1);
let concurrency = (sources * max_fetch_tasks.max(1)).min(MAX_PLANNING_INDEX_READS);
let anchors: Vec<(Bytes, u64)> = stream::iter(views)
.map(|view| {
let table_store = table_store.clone();
async move {
let index = table_store.read_index(&view.sst, true).await?;
Ok::<_, SlateDBError>(sample_anchors(
&index,
view.sst.info.filter_offset,
MAX_ANCHORS_PER_SST,
view.compacted_effective_range(),
))
}
})
.buffer_unordered(concurrency)
.try_collect::<Vec<_>>()
.await?
.into_iter()
.flatten()
.collect();
Ok(select_boundaries(
anchors,
max_subcompactions,
min_range_bytes,
))
}
fn select_boundaries(
mut anchors: Vec<(Bytes, u64)>,
max_subcompactions: usize,
min_range_bytes: u64,
) -> Vec<BytesRange> {
if max_subcompactions <= 1 {
return vec![BytesRange::unbounded()];
}
let total: u64 = anchors.iter().map(|(_, bytes)| bytes).sum();
let target = (total / max_subcompactions as u64).max(min_range_bytes);
if target == 0 || target >= total {
return vec![BytesRange::unbounded()];
}
anchors.sort_by(|a, b| a.0.cmp(&b.0));
let first_key = anchors[0].0.clone();
let mut boundaries: Vec<Bytes> = Vec::new();
let mut threshold = target;
let mut cumulative = 0u64;
for (key, bytes) in anchors {
let distinct = match boundaries.last() {
Some(last) => key > *last,
None => key > first_key,
};
if cumulative >= threshold && boundaries.len() < max_subcompactions - 1 && distinct {
boundaries.push(key);
threshold += target;
}
cumulative += bytes;
}
if boundaries.is_empty() {
return vec![BytesRange::unbounded()];
}
let mut ranges = Vec::with_capacity(boundaries.len() + 1);
let mut start: Bound<Bytes> = Unbounded;
for boundary in boundaries {
ranges.push(BytesRange::new(start, Excluded(boundary.clone())));
start = Included(boundary);
}
ranges.push(BytesRange::new(start, Unbounded));
ranges
}
fn sample_anchors(
index: &SsTableIndexOwned,
data_end_offset: u64,
max_anchors: usize,
effective_range: &BytesRange,
) -> Vec<(Bytes, u64)> {
let borrowed = index.borrow();
let blocks = borrowed.block_meta();
let num_blocks = blocks.len();
if num_blocks == 0 {
return Vec::new();
}
let block_end = |i: usize| -> u64 {
if i + 1 < num_blocks {
blocks.get(i + 1).offset()
} else {
data_end_offset
}
};
let max_anchors = max_anchors.max(1);
let stride = num_blocks.div_ceil(max_anchors);
let mut anchors = Vec::with_capacity(num_blocks.div_ceil(stride));
let mut block = 0;
while block < num_blocks {
let first = blocks.get(block);
let key = Bytes::copy_from_slice(first.first_key().bytes());
let group_end = (block + stride).min(num_blocks);
let bytes = block_end(group_end - 1).saturating_sub(first.offset());
if effective_range.contains(&key) {
anchors.push((key, bytes));
}
block = group_end;
}
anchors
}
#[cfg(test)]
mod tests {
use super::*;
use std::ops::RangeBounds;
fn k(i: u64) -> Bytes {
Bytes::copy_from_slice(format!("k{i:010}").as_bytes())
}
fn build_index(blocks: &[(Bytes, u64)]) -> SsTableIndexOwned {
use crate::flatbuffer_types::{BlockMeta, BlockMetaArgs, SsTableIndex, SsTableIndexArgs};
let mut fbb = flatbuffers::FlatBufferBuilder::new();
let metas: Vec<_> = blocks
.iter()
.map(|(key, offset)| {
let first_key = fbb.create_vector(key);
BlockMeta::create(
&mut fbb,
&BlockMetaArgs {
offset: *offset,
first_key: Some(first_key),
},
)
})
.collect();
let vector = fbb.create_vector(&metas);
let index = SsTableIndex::create(
&mut fbb,
&SsTableIndexArgs {
block_meta: Some(vector),
},
);
fbb.finish(index, None);
SsTableIndexOwned::new(Bytes::copy_from_slice(fbb.finished_data())).unwrap()
}
fn assert_covering_ranges(ranges: &[BytesRange]) {
assert!(!ranges.is_empty());
assert_eq!(RangeBounds::start_bound(ranges.first().unwrap()), Unbounded);
assert_eq!(RangeBounds::end_bound(ranges.last().unwrap()), Unbounded);
for range in ranges {
assert!(range.non_empty(), "range must be non-empty: {range:?}");
}
for pair in ranges.windows(2) {
let Excluded(prev_end) = pair[0].end_bound() else {
panic!("non-terminal ranges must have excluded end bounds");
};
let Included(next_start) = pair[1].start_bound() else {
panic!("non-initial ranges must have included start bounds");
};
assert_eq!(prev_end, next_start, "adjacent ranges must be contiguous");
}
}
#[test]
fn test_should_not_split_when_subcompactions_disabled() {
let anchors = vec![(k(0), 100), (k(100), 100)];
let ranges = select_boundaries(anchors, 1, 1);
assert_eq!(ranges, vec![BytesRange::unbounded()]);
}
#[test]
fn test_should_not_split_inputs_below_floor() {
let anchors = vec![(k(0), 100), (k(100), 100)];
let ranges = select_boundaries(anchors, 4, 1000);
assert_eq!(ranges, vec![BytesRange::unbounded()]);
}
#[test]
fn test_should_split_evenly_weighted_anchors() {
let anchors = vec![(k(0), 100), (k(10), 100), (k(20), 100), (k(30), 100)];
let ranges = select_boundaries(anchors, 4, 1);
assert_covering_ranges(&ranges);
assert_eq!(ranges.len(), 4);
assert_eq!(ranges[0].end_bound(), Excluded(&k(10)));
assert_eq!(ranges[1].end_bound(), Excluded(&k(20)));
assert_eq!(ranges[2].end_bound(), Excluded(&k(30)));
}
#[test]
fn test_should_divide_evenly_with_many_anchors_per_subcompaction() {
let anchors: Vec<(Bytes, u64)> = (0..16).map(|i| (k(i), 10)).collect();
let ranges = select_boundaries(anchors, 4, 25);
assert_covering_ranges(&ranges);
assert_eq!(ranges.len(), 4);
assert_eq!(ranges[0].end_bound(), Excluded(&k(4)));
assert_eq!(ranges[1].end_bound(), Excluded(&k(8)));
assert_eq!(ranges[2].end_bound(), Excluded(&k(12)));
}
#[test]
fn test_should_split_into_fewer_ranges_when_floor_binds() {
let anchors: Vec<(Bytes, u64)> = (0..8).map(|i| (k(i), 50)).collect();
let ranges = select_boundaries(anchors, 8, 100);
assert_covering_ranges(&ranges);
assert_eq!(ranges.len(), 4);
assert_eq!(ranges[0].end_bound(), Excluded(&k(2)));
assert_eq!(ranges[1].end_bound(), Excluded(&k(4)));
assert_eq!(ranges[2].end_bound(), Excluded(&k(6)));
}
#[test]
fn test_should_cap_splits_at_max_subcompactions() {
let anchors: Vec<(Bytes, u64)> = (0..100).map(|i| (k(i), 100)).collect();
let ranges = select_boundaries(anchors, 4, 1);
assert_covering_ranges(&ranges);
assert_eq!(ranges.len(), 4, "must not exceed max_subcompactions ranges");
}
#[test]
fn test_should_isolate_heavy_region() {
let anchors = vec![
(k(0), 1000),
(k(10), 10),
(k(20), 10),
(k(30), 10),
(k(40), 10),
];
let ranges = select_boundaries(anchors, 2, 1);
assert_covering_ranges(&ranges);
assert_eq!(ranges.len(), 2);
assert_eq!(ranges[0].end_bound(), Excluded(&k(10)));
}
#[test]
fn test_should_not_split_single_anchor() {
let anchors = vec![(k(0), 1000)];
let ranges = select_boundaries(anchors, 4, 1000);
assert_eq!(ranges, vec![BytesRange::unbounded()]);
}
#[test]
fn test_should_not_emit_boundary_on_smallest_key() {
let anchors = vec![
(k(0), 400),
(k(0), 400),
(k(0), 400),
(k(10), 400),
(k(20), 400),
];
let ranges = select_boundaries(anchors, 4, 1);
assert_covering_ranges(&ranges);
assert_eq!(ranges[0].end_bound(), Excluded(&k(10)));
assert!(ranges.len() > 1);
}
#[test]
fn test_should_handle_duplicate_keys_without_empty_ranges() {
let anchors = vec![
(k(0), 500),
(k(0), 500),
(k(10), 500),
(k(10), 500),
(k(20), 500),
(k(20), 500),
];
let ranges = select_boundaries(anchors, 4, 1);
assert_covering_ranges(&ranges);
assert!(ranges.len() > 1);
}
#[test]
fn test_sample_anchors_keeps_all_blocks_without_projection() {
let index = build_index(&[(k(0), 0), (k(10), 100), (k(20), 200), (k(30), 300)]);
let anchors = sample_anchors(&index, 400, MAX_ANCHORS_PER_SST, &BytesRange::unbounded());
assert_eq!(
anchors,
vec![(k(0), 100), (k(10), 100), (k(20), 100), (k(30), 100)]
);
}
#[test]
fn test_sample_anchors_clips_to_projected_effective_range() {
let index = build_index(&[(k(0), 0), (k(10), 100), (k(20), 200), (k(30), 300)]);
let effective_range = BytesRange::new(Included(k(10)), Excluded(k(30)));
let anchors = sample_anchors(&index, 400, MAX_ANCHORS_PER_SST, &effective_range);
assert_eq!(anchors, vec![(k(10), 100), (k(20), 100)]);
}
}