use super::SpillRun;
use crate::concurrent::BoundedExecutor;
use crate::pipeline::runtime_checkpoint;
use hawdb_core::{HawDBError, Result, RuntimeTaskContext};
use std::num::NonZeroUsize;
pub(crate) fn default_compaction_worker_limit() -> NonZeroUsize {
NonZeroUsize::new(4).unwrap()
}
pub(crate) struct CompactionMemory<'a> {
pub blocking: &'a crate::QueryMemoryAccount,
pub spill: &'a crate::kernel::SpillBudgetTracker,
pub worker_limit: NonZeroUsize,
}
pub(crate) fn compact_runs_with_memory(
runs: Vec<SpillRun>,
final_run_count: NonZeroUsize,
memory: CompactionMemory<'_>,
task_context: Option<&RuntimeTaskContext>,
merge_pair: impl Fn(
&SpillRun,
&SpillRun,
&crate::QueryMemoryAccount,
&crate::kernel::SpillBudgetTracker,
) -> Result<SpillRun>
+ Sync,
) -> Result<Vec<SpillRun>> {
compact_levels(runs, final_run_count, task_context, |pairs| {
let serial_budget = memory.blocking.available_bytes().max(1);
let mut outputs = Vec::with_capacity(pairs.len());
let mut offset = 0;
while offset < pairs.len() {
runtime_checkpoint(task_context)?;
let mut wave = Vec::new();
for (index, (left, right)) in pairs
.iter()
.enumerate()
.skip(offset)
.take(memory.worker_limit.get())
{
let opaque = left.merge_record_bytes() == 0 || right.merge_record_bytes() == 0;
let record_bytes = if opaque {
serial_budget
} else {
left.merge_record_bytes()
.saturating_add(right.merge_record_bytes())
.min(serial_budget)
};
let staging_bytes = if opaque {
memory.spill.staging_budget_bytes()
} else {
left.staging_bytes()
.saturating_add(right.staging_bytes())
.min(memory.spill.staging_budget_bytes())
.max(1)
};
let admitted = (|| {
let blocking = memory
.blocking
.sub_account(NonZeroUsize::new(record_bytes.max(1)).unwrap())?;
let spill = memory
.spill
.for_merge(NonZeroUsize::new(staging_bytes).unwrap())?;
Ok::<_, HawDBError>((index, blocking, spill))
})();
match admitted {
Ok(merge) => wave.push(merge),
Err(_) => break, }
}
if wave.is_empty() {
let (left, right) = &pairs[offset];
outputs.push(merge_pair(left, right, memory.blocking, memory.spill)?);
offset += 1;
continue;
}
let executor = BoundedExecutor::new(memory.worker_limit);
let execute = |(index, blocking, spill): &(
usize,
crate::QueryMemoryAccount,
crate::kernel::SpillBudgetTracker,
)| {
let (left, right) = &pairs[*index];
merge_pair(left, right, blocking, spill)
};
let results: Vec<Result<SpillRun>> = match task_context {
Some(context) => executor
.map_ordered_with_context(&wave, context, execute)
.map_err(|reason| {
HawDBError::Execution(format!("runtime task stopped: {reason}"))
})?,
None => executor.map_ordered(&wave, execute),
};
offset += wave.len();
outputs.extend(results.into_iter().collect::<Result<Vec<_>>>()?);
}
Ok(outputs)
})
}
fn compact_levels(
mut runs: Vec<SpillRun>,
final_run_count: NonZeroUsize,
task_context: Option<&RuntimeTaskContext>,
merge: impl Fn(&[(SpillRun, SpillRun)]) -> Result<Vec<SpillRun>>,
) -> Result<Vec<SpillRun>> {
while runs.len() > final_run_count.get() {
runtime_checkpoint(task_context)?;
let mut pending = runs.into_iter();
let mut pairs = Vec::with_capacity(pending.len() / 2);
let mut leftover = None;
while let Some(left) = pending.next() {
match pending.next() {
Some(right) => pairs.push((left, right)),
None => leftover = Some(left),
}
}
let mut compacted = merge(&pairs)?;
compacted.extend(leftover);
runs = compacted;
}
Ok(runs)
}
#[cfg(test)]
mod tests;