mod commit;
mod fifo;
mod job;
mod levels;
pub(super) mod manual;
mod output;
mod pick;
mod request;
mod retain;
mod split;
mod universal;
#[cfg(test)]
mod fifo_tests;
#[cfg(test)]
mod job_tests;
#[cfg(test)]
mod pick_tests;
#[cfg(test)]
mod retain_tests;
#[cfg(test)]
mod running_tests;
#[cfg(test)]
mod split_tests;
#[cfg(test)]
mod universal_tests;
use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::{Arc, Weak};
use tessarikv_constants::LEVELS;
use tessarikv_format::TableMeta;
use tokio::task::JoinSet;
use self::job::Job;
pub(super) use self::levels::owed;
use self::levels::shape_of;
use self::manual::{Active, Request};
use self::pick::{Busy, Pick, Shape};
use super::block_cache::BlockCache;
use super::installer::Flusher;
use super::table_blocks::TableBlocks;
use super::view::TableFile;
use crate::storage::Storage;
use crate::{CompactionStyle, KeyspaceOptions};
use crate::{Error, Result};
pub(super) const SWEEP_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100);
type Finished = (u64, Result<Vec<TableFile>>);
#[derive(Default)]
pub(super) struct Compactions {
jobs: JoinSet<Finished>,
running: Vec<(u64, u32, Pick)>,
next_job: u64,
ready: Vec<(u64, u32, Pick, Vec<TableFile>)>,
obsolete: Vec<(Weak<TableBlocks>, u64, PathBuf)>,
queued: VecDeque<Request>,
active: Option<Active>,
}
impl Compactions {
pub(super) fn has_obsolete(&self) -> bool {
!self.obsolete.is_empty()
}
pub(super) fn is_running(&self) -> bool {
!self.jobs.is_empty()
}
pub(super) async fn finished(&mut self) -> Result<()> {
let Some(joined) = self.jobs.join_next().await else {
return std::future::pending().await;
};
let (number, outputs) = joined.map_err(|_| Error::CompactionFailed)?;
let at = self
.running
.iter()
.position(|(running, _, _)| *running == number)
.ok_or(Error::CompactionFailed)?;
let (_, keyspace, pick) = self.running.swap_remove(at);
self.ready.push((number, keyspace, pick, outputs?));
Ok(())
}
pub(super) fn request(&mut self, request: Request) {
self.queued.push_back(request);
}
pub(super) async fn step(&mut self, flusher: &mut Flusher, start: bool) -> Result<bool> {
let mut published = false;
if flusher.manifest.is_none() {
return Ok(published);
}
for (number, keyspace, pick, outputs) in std::mem::take(&mut self.ready) {
if flusher.holds(keyspace) {
self.commit(flusher, keyspace, &pick, outputs).await?;
published = true;
} else {
self.retire(outputs, &flusher.dir);
}
self.recorded(number);
}
self.refuse_unheld(flusher);
self.sweep(Arc::clone(&flusher.storage), Arc::clone(&flusher.cache))
.await;
let mut index = 0_usize;
while start
&& let Some(keyspace) = index.checked_sub(1).map_or(Some(0), |named| {
flusher.keyspaces.get(named).map(|(id, _)| *id)
})
{
published |= self.start_jobs(flusher, keyspace).await?;
index = index.saturating_add(1);
}
Ok(published)
}
async fn start_jobs(&mut self, flusher: &mut Flusher, keyspace: u32) -> Result<bool> {
let mut published = false;
while self.has_room(keyspace, flusher.keyspace_options(keyspace)) {
let Some((levels, shape)) = shape_of(flusher, keyspace) else {
break;
};
let busy = self.busy(keyspace);
let (next, requested) = match self.manual_step(flusher, keyspace, &levels, shape, &busy)
{
manual::Next::Run(pick) => (Some(pick), true),
manual::Next::Blocked => break,
manual::Next::Idle => {
let options = flusher.keyspace_options(keyspace);
(automatic(options, &levels, shape, &busy), false)
}
};
let Some(pick) = next else {
break;
};
if pick.is_move() || pick.drop {
self.commit(flusher, keyspace, &pick, Vec::new()).await?;
published = true;
continue;
}
let job = Job::start(flusher, keyspace, &pick, &levels)?;
let number = self.next_job;
self.next_job = number.wrapping_add(1);
if requested && let Some(active) = &mut self.active {
active.waiting = Some(number);
}
self.running.push((number, keyspace, pick));
self.jobs.spawn_blocking(move || (number, job.run()));
}
Ok(published)
}
fn has_room(&self, keyspace: u32, options: &KeyspaceOptions) -> bool {
let most = usize::try_from(options.max_compactions()).unwrap_or(usize::MAX);
let running = self
.running
.iter()
.filter(|(_, running, _)| *running == keyspace)
.count();
running < most
}
fn busy(&self, keyspace: u32) -> Busy {
let mut busy = Busy::default();
for (_, _, pick) in self.running.iter().filter(|(_, held, _)| *held == keyspace) {
busy.tables.extend(
pick.inputs
.iter()
.chain(&pick.overlaps)
.map(|table| table.id),
);
busy.ranges.extend(pick.busy_range());
}
busy
}
async fn sweep(&mut self, storage: Arc<dyn Storage>, cache: Arc<BlockCache>) {
let (unread, read): (Vec<_>, Vec<_>) = std::mem::take(&mut self.obsolete)
.into_iter()
.partition(|(blocks, _, _)| blocks.strong_count() == 0);
self.obsolete = read;
if unread.is_empty() {
return;
}
let failed = tokio::task::spawn_blocking(move || {
unread
.into_iter()
.filter(|(_, id, path)| !cache.forget_file(*id) || storage.remove(path).is_err())
.collect::<Vec<_>>()
})
.await
.unwrap_or_default();
self.obsolete.extend(failed);
}
}
fn automatic(
options: &KeyspaceOptions,
levels: &[Vec<Arc<TableMeta>>; LEVELS],
shape: Shape,
busy: &Busy,
) -> Option<Pick> {
if options.disable_auto_compactions() {
return None;
}
match options.compaction() {
CompactionStyle::Fifo { max_table_bytes } => fifo::pick(levels, max_table_bytes, busy),
CompactionStyle::Universal {
size_ratio_percent,
min_merge,
max_size_amplification_percent,
} => universal::pick(
levels,
universal::Settings {
level0_trigger: shape.level0_trigger,
size_ratio_percent,
min_merge,
max_size_amplification_percent,
},
busy,
),
_ => pick::pick_with(levels, shape, busy),
}
}