use std::sync::Arc;
use tessarikv_constants::LEVELS;
use tessarikv_format::{TableIter, TableMeta};
use tessarikv_types::{
InternalKeyRef, SequenceNumber, ValueKind, compare_internal, encode_internal, seek_trailer,
};
use super::output::{Destination, Outputs};
use super::pick::Pick;
use super::retain::Retention;
use super::split;
use crate::db::flush_job::table_options;
use crate::db::installer::Flusher;
use crate::db::tables::{self, format_error, user_key};
use crate::db::view::TableFile;
use crate::statistics::{Statistics, Ticker};
use crate::tombstones::RangeTombstone;
use crate::{Error, Result};
pub(crate) struct Job {
pub(crate) to: Destination,
pub(crate) inputs: Vec<TableFile>,
pub(crate) deeper: Vec<Vec<Arc<TableMeta>>>,
pub(crate) retention: Retention,
pub(crate) parts: usize,
}
struct Walk<'a> {
versions: TableIter,
file: &'a TableFile,
}
impl Job {
pub(super) fn start(
flusher: &Flusher,
keyspace: u32,
pick: &Pick,
levels: &[Vec<Arc<TableMeta>>; LEVELS],
) -> Result<Self> {
let options = flusher.keyspace_options(keyspace);
let current = flusher.view.load();
let wanted: Vec<u64> = pick
.inputs
.iter()
.chain(&pick.overlaps)
.map(|table| table.id)
.collect();
let inputs: Vec<TableFile> = current
.files(keyspace)
.filter(|file| wanted.contains(&file.meta.id))
.cloned()
.collect();
if inputs.len() != wanted.len() {
return Err(Error::CompactionFailed);
}
let horizon = flusher.snapshots.horizon(flusher.sequencer.visible());
Ok(Self {
to: Destination {
storage: Arc::clone(&flusher.storage),
dir: flusher.dir.clone(),
options: table_options(options, pick.output_level)?,
target_bytes: if pick.output_level == 0 {
u64::MAX
} else {
options.target_file_bytes()
},
numbers: Arc::clone(&flusher.numbers),
cache: Arc::clone(&flusher.cache),
keyspace,
level: u8::try_from(pick.output_level).map_err(|_| Error::CompactionFailed)?,
limiter: flusher.limiter.clone(),
},
inputs,
deeper: deeper(pick, levels, &wanted),
retention: Retention::new(horizon.visible(), horizon.snapshots()),
parts: if pick.output_level == 0 {
1
} else {
usize::try_from(options.max_subcompactions()).unwrap_or(1)
},
})
}
pub(crate) fn run(self) -> Result<Vec<TableFile>> {
let mut tombstones = Vec::new();
for file in &self.inputs {
tables::tombstones(&file.table, &mut tombstones)?;
}
let cuts = split::boundaries(
self.inputs
.iter()
.map(|file| user_key(&file.meta.smallest).to_vec()),
self.parts,
);
let written = if cuts.is_empty() {
self.merge(None, None, &tombstones)?
} else {
let lows = std::iter::once(None).chain(cuts.iter().map(|cut| Some(cut.as_slice())));
let highs = cuts.iter().map(|cut| Some(cut.as_slice())).chain([None]);
let job = &self;
let tombstones = &tombstones;
std::thread::scope(|scope| {
let parts: Vec<_> = lows
.zip(highs)
.map(|(low, high)| {
std::thread::Builder::new()
.name("tessarikv-subcompaction".to_owned())
.spawn_scoped(scope, move || job.merge(low, high, tombstones))
})
.collect();
let mut written = Vec::new();
for part in parts {
let part = part.map_err(|_| Error::CompactionFailed)?;
written.extend(part.join().map_err(|_| Error::CompactionFailed)??);
}
Ok::<_, Error>(written)
})?
};
if !written.is_empty() {
self.to.storage.sync_dir(&self.to.dir)?;
}
if let Some(statistics) = self.to.cache.statistics() {
count(statistics, &self.inputs, &written);
}
Ok(written)
}
fn merge(
&self,
low: Option<&[u8]>,
high: Option<&[u8]>,
tombstones: &[RangeTombstone],
) -> Result<Vec<TableFile>> {
let inside =
|key: &[u8]| low.is_none_or(|low| low <= key) && high.is_none_or(|high| key < high);
let tombstones: Vec<RangeTombstone> = tombstones
.iter()
.filter(|tombstone| inside(&tombstone.start))
.cloned()
.collect();
let mut outputs = Outputs::new(&self.to, &tombstones);
let mut target = Vec::new();
if let Some(low) = low {
encode_internal(low, seek_trailer(SequenceNumber::MAX), &mut target);
}
let mut walks = Vec::with_capacity(self.inputs.len());
for file in &self.inputs {
let mut versions = file.table.iter();
if target.is_empty() {
versions.seek_to_first();
} else {
versions.seek(&target);
}
settle(file, &mut versions)?;
walks.push(Walk { versions, file });
}
let mut key: Vec<u8> = Vec::new();
let mut decisions = self.retention.key();
let mut first = true;
while let Some(walk) = walks
.iter_mut()
.filter(|walk| walk.versions.valid())
.min_by(|left, right| compare_internal(left.versions.key(), right.versions.key()))
{
let stored = walk.versions.key();
let parsed = InternalKeyRef::parse(stored).map_err(|_| corrupt())?;
if !inside(parsed.user_key()) {
break;
}
let kind = parsed.trailer().kind().map_err(|_| corrupt())?;
let starts_key = first || parsed.user_key() != key.as_slice();
if starts_key {
key.clear();
key.extend_from_slice(parsed.user_key());
decisions = self.retention.key();
first = false;
}
let sequence = parsed.trailer().sequence().get();
let keep = decisions.keeps(sequence, kind, || deeper_holds(&self.deeper, &key));
if keep && matches!(kind, ValueKind::Value | ValueKind::Delete) {
outputs.add(stored, starts_key, walk.versions.value())?;
}
walk.versions.next();
settle(walk.file, &mut walk.versions)?;
}
outputs.finish()
}
}
pub(super) fn deeper(
pick: &Pick,
levels: &[Vec<Arc<TableMeta>>; LEVELS],
merged: &[u64],
) -> Vec<Vec<Arc<TableMeta>>> {
let older_runs = levels
.first()
.filter(|_| pick.output_level == 0)
.into_iter()
.flatten()
.filter(|table| !merged.contains(&table.id))
.map(|table| vec![Arc::clone(table)]);
older_runs
.chain(
levels
.iter()
.skip(pick.output_level.saturating_add(1))
.cloned(),
)
.collect()
}
fn settle(file: &TableFile, versions: &mut TableIter) -> Result<()> {
file.blocks.complete_blocking(versions)?;
versions
.status()
.map_err(|error| format_error(&error, None))
}
fn deeper_holds(deeper: &[Vec<Arc<TableMeta>>], key: &[u8]) -> bool {
deeper.iter().any(|level| {
let at = level.partition_point(|table| user_key(&table.largest) < key);
level
.get(at)
.is_some_and(|table| user_key(&table.smallest) <= key)
})
}
fn corrupt() -> Error {
Error::Corruption {
what: "table",
path: None,
offset: None,
detail: "a stored key has no trailer or a kind this build does not read".to_owned(),
}
}
fn count(statistics: &Statistics, inputs: &[TableFile], outputs: &[TableFile]) {
let bytes = |files: &[TableFile]| {
files
.iter()
.fold(0_u64, |total, file| total.saturating_add(file.meta.size))
};
let entries = |files: &[TableFile]| {
files.iter().fold(0_u64, |total, file| {
total.saturating_add(file.table.entries())
})
};
statistics.record(Ticker::CompactionBytesRead, bytes(inputs));
statistics.record(Ticker::CompactionBytesWritten, bytes(outputs));
let dropped = entries(inputs).saturating_sub(entries(outputs));
statistics.record(Ticker::CompactionKeysDropped, dropped);
}